web-dev-qa-db-fra.com

Tri alphanumérique avec LINQ

J'ai un string[] dans lequel chaque élément se termine par une valeur numérique.

string[] partNumbers = new string[] 
{ 
    "ABC10", "ABC1","ABC2", "ABC11","ABC10", "AB1", "AB2", "Ab11" 
};

J'essaie de trier le tableau ci-dessus comme suit en utilisant LINQ mais je n'obtiens pas le résultat attendu.

var result = partNumbers.OrderBy(x => x);

Résultat actuel:

AB1
Ab11
AB2
ABC1
ABC10
ABC10
ABC11
ABC2 

Résultat attendu

AB1
AB2
AB11
..

27
santosh singh

Cela est dû au fait que l'ordre par défaut pour string est l'ordre du dictionnaire alphanumérique standard (lexicographique) et qu'ABC11 passera avant ABC2 car l'ordre se déroule toujours de gauche à droite.

Pour obtenir ce que vous voulez, vous devez remplir la partie numérique de votre clause order by, comme suit:

 var result = partNumbers.OrderBy(x => PadNumbers(x));

PadNumbers pourrait être défini comme:

public static string PadNumbers(string input)
{
    return Regex.Replace(input, "[0-9]+", match => match.Value.PadLeft(10, '0'));
}

Cela remplit les zéros des nombres (ou des nombres) apparaissant dans la chaîne d'entrée, de sorte que OrderBy voit:

ABC0000000010
ABC0000000001
...
AB0000000011

Le remplissage ne se produit que sur la clé utilisée pour la comparaison. Les chaînes d'origine (sans remplissage) sont conservées dans le résultat.

Notez que cette approche suppose un nombre maximal de chiffres pour les nombres en entrée.

35
Nathan

Une implémentation appropriée d'une méthode de tri alphanumérique «juste fonctionne» est disponible sur le site de Dave Koelle . La version C # est ici

9
ranieuwe

Si vous souhaitez trier une liste d'objets en fonction d'une propriété spécifique à l'aide de LINQ et d'un comparateur personnalisé tel que celui de Dave Koelle vous feriez quelque chose comme ceci:

...

items = items.OrderBy(x => x.property, new AlphanumComparator()).ToList();

...

Vous devez également modifier la classe de Dave pour hériter de System.Collections.Generic.IComparer<object> au lieu de la IComparer de base afin que la signature de la classe devienne:

...

public class AlphanumComparator : System.Collections.Generic.IComparer<object>
{

    ...

Personnellement, je préfère l’implémentation de James McCormack car elle implémente IDisposable, bien que mon analyse comparative montre qu’elle est légèrement plus lente. 

4
John Meyer

Vous pouvez PInvoke to StrCmpLogicalW (la fonction windows) pour le faire. Voir ici: Ordre de tri naturel en C #

3
Mark Sowul
public class AlphanumComparatorFast : IComparer
{
    List<string> GetList(string s1)
    {
        List<string> SB1 = new List<string>();
        string st1, st2, st3;
        st1 = "";
        bool flag = char.IsDigit(s1[0]);
        foreach (char c in s1)
        {
            if (flag != char.IsDigit(c) || c=='\'')
            {
                if(st1!="")
                SB1.Add(st1);
                st1 = "";
                flag = char.IsDigit(c);
            }
            if (char.IsDigit(c))
            {
                st1 += c;
            }
            if (char.IsLetter(c))
            {
                st1 += c;
            }


        }
        SB1.Add(st1);
        return SB1;
    }



    public int Compare(object x, object y)
    {
        string s1 = x as string;
        if (s1 == null)
        {
            return 0;
        }
        string s2 = y as string;
        if (s2 == null)
        {
            return 0;
        }
        if (s1 == s2)
        {
            return 0;
        }
        int len1 = s1.Length;
        int len2 = s2.Length;
        int marker1 = 0;
        int marker2 = 0;

        // Walk through two the strings with two markers.
        List<string> str1 = GetList(s1);
        List<string> str2 = GetList(s2);
        while (str1.Count != str2.Count)
        {
            if (str1.Count < str2.Count)
            {
                str1.Add("");
            }
            else
            {
                str2.Add("");
            }
        }
        int x1 = 0; int res = 0; int x2 = 0; string y2 = "";
        bool status = false;
        string y1 = ""; bool s1Status = false; bool s2Status = false;
        //s1status ==false then string ele int;
        //s2status ==false then string ele int;
        int result = 0;
        for (int i = 0; i < str1.Count && i < str2.Count; i++)
        {
            status = int.TryParse(str1[i].ToString(), out res);
            if (res == 0)
            {
                y1 = str1[i].ToString();
                s1Status = false;
            }
            else
            {
                x1 = Convert.ToInt32(str1[i].ToString());
                s1Status = true;
            }

            status = int.TryParse(str2[i].ToString(), out res);
            if (res == 0)
            {
                y2 = str2[i].ToString();
                s2Status = false;
            }
            else
            {
                x2 = Convert.ToInt32(str2[i].ToString());
                s2Status = true;
            }
            //checking --the data comparision
            if(!s2Status && !s1Status )    //both are strings
            {
                result = str1[i].CompareTo(str2[i]);
            }
            else if (s2Status && s1Status) //both are intergers
            {
                if (x1 == x2)
                {
                    if (str1[i].ToString().Length < str2[i].ToString().Length)
                    {
                        result = 1;
                    }
                    else if (str1[i].ToString().Length > str2[i].ToString().Length)
                        result = -1;
                    else
                        result = 0;
                }
                else
                {
                    int st1ZeroCount=str1[i].ToString().Trim().Length- str1[i].ToString().TrimStart(new char[]{'0'}).Length;
                    int st2ZeroCount = str2[i].ToString().Trim().Length - str2[i].ToString().TrimStart(new char[] { '0' }).Length;
                    if (st1ZeroCount > st2ZeroCount)
                        result = -1;
                    else if (st1ZeroCount < st2ZeroCount)
                        result = 1;
                    else
                    result = x1.CompareTo(x2);

                }
            }
            else
            {
                result = str1[i].CompareTo(str2[i]);
            }
            if (result == 0)
            {
                continue;
            }
            else
                break;

        }
        return result;
    }
}

USAGE de cette classe:

    List<string> marks = new List<string>();
                marks.Add("M'00Z1");
                marks.Add("M'0A27");
                marks.Add("M'00Z0");
marks.Add("0000A27");
                marks.Add("100Z0");

    string[] Markings = marks.ToArray();

                Array.Sort(Markings, new AlphanumComparatorFast());
3
Sathish adabala

Vous pouvez utiliser PInvoke pour obtenir un résultat rapide et satisfaisant:

class AlphanumericComparer : IComparer<string>
{
    [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
    static extern int StrCmpLogicalW(string s1, string s2);

    public int Compare(string x, string y) => StrCmpLogicalW(x, y);
}

Vous pouvez l'utiliser comme AlphanumComparatorFast de la réponse ci-dessus.

3
Alex Zhukovskiy

On dirait bien qu’il fait un ordre lexicographique, qu’il soit petit ou majuscule.

Vous pouvez essayer d'utiliser une expression personnalisée dans cette lambda pour le faire.

1
Shekhar_Pro

Il n'y a pas de moyen naturel de faire cela dans .NET, mais jetez un coup d'œil à ce billet de blog sur le tri naturel

Vous pouvez mettre cela dans une méthode d'extension et l'utiliser à la place de OrderBy

1
AndrewC

Comme le nombre de caractères au début est variable, une expression régulière aiderait:

var re = new  Regex(@"\d+$"); // finds the consecutive digits at the end of the string
var result = partNumbers.OrderBy(x => int.Parse(re.Match(x).Value));

S'il existe un nombre fixe de caractères de préfixe, vous pouvez utiliser la méthode Substring pour extraire à partir des caractères appropriés:

// parses the string as a number starting from the 5th character
var result = partNumbers.OrderBy(x => int.Parse(x.Substring(4)));

Si les nombres peuvent contenir un séparateur décimal ou un séparateur de milliers, l'expression régulière doit également autoriser ces caractères:

var re = new Regex(@"[\d,]*\.?\d+$");
var result = partNumbers.OrderBy(x => double.Parse(x.Substring(4)));

Si la chaîne renvoyée par l'expression régulière ou par Substring peut être non analysable par int.Parse/double.Parse, utilisez la variante TryParse appropriée:

var re = new  Regex(@"\d+$"); // finds the consecutive digits at the end of the string
var result = partNumbers.OrderBy(x => {
    int? parsed = null;
    if (int.TryParse(re.Match(x).Value, out var temp)) {
        parsed = temp;
    }
    return parsed;
});
0
Zev Spitz