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
..
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));
où 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.
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 .
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.
Vous pouvez PInvoke to StrCmpLogicalW
(la fonction windows) pour le faire. Voir ici: Ordre de tri naturel en C #
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());
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.
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.
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
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;
});