Clasificación alfanumérica con LINQ

Tengo una string[] en la que cada elemento termina con algún valor numérico.

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

Estoy tratando de ordenar la matriz anterior de la siguiente manera utilizando LINQ pero no estoy obteniendo el resultado esperado.

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

Resultado actual:

AB1
Ab11
AB2
ABC1
ABC10
ABC10
ABC11
ABC2

Resultado Esperado

AB1
AB2
AB11
..

Esto se debe a que el orden predeterminado para la cadena es el orden alfabético estándar del diccionario (lexicográfico), y ABC11 vendrá antes que ABC2 porque el orden siempre procede de izquierda a derecha.

Para obtener lo que desea, debe rellenar la parte numérica en su orden por cláusula, algo así como:

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

donde PadNumbers podría definirse como:

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

Esto almohadilla los ceros para cualquier número (o números) que aparezca en la cadena de entrada para que OrderBy vea:

 ABC0000000010 ABC0000000001 ... AB0000000011 

El relleno solo ocurre en la clave utilizada para la comparación. Las cadenas originales (sin relleno) se conservan en el resultado.

Tenga en cuenta que este enfoque supone una cantidad máxima de dígitos para los números en la entrada.

Puede encontrar una implementación adecuada de un método de clasificación alfanumérico que ‘simplemente funciona’ en el sitio de Dave Koelle . La versión de C # está aquí .

 public class AlphanumComparatorFast : IComparer { List GetList(string s1) { List SB1 = new List(); 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 str1 = GetList(s1); List 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; } } 

USO de esta clase:

  List marks = new List(); 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()); 

Si desea ordenar una lista de objetos por una propiedad específica utilizando LINQ y un comparador personalizado como el de Dave Koelle , haría algo como esto:

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

También debe alterar la clase de Dave para heredar de System.Collections.Generic.IComparer lugar del IComparer básico para que la firma de clase se convierta en:

 ... public class AlphanumComparator : System.Collections.Generic.IComparer { ... 

Personalmente, prefiero la implementación de James McCormack porque implementa IDisposable, aunque mi evaluación comparativa muestra que es un poco más lenta.

Puede usar PInvoke para obtener un resultado rápido y bueno:

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

Puede usarlo como AlphanumComparatorFast desde la respuesta anterior.

Puede invocar a StrCmpLogicalW (la función de Windows) para hacer esto. Vea aquí: Orden de clasificación natural en C #

Parece que está haciendo un pedido lexicográfico, independientemente de los caracteres pequeños o mayúsculas.

Puedes intentar usar alguna expresión personalizada en esa lambda para hacer eso.

No hay una forma natural de hacerlo en .NET, pero eche un vistazo a esta publicación de blog sobre clasificación natural

Puede poner esto en un método de extensión y usarlo en lugar de OrderBy

Como el número de caracteres al principio es variable, una expresión regular ayudaría:

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

Si hubiera un número fijo de caracteres de prefijo, entonces podría usar el método de Substring para extraer a partir de los caracteres relevantes:

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

Si los números pueden contener un separador decimal o un separador de miles, entonces la expresión regular también debe permitir esos caracteres:

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

Si la cadena devuelta por la expresión regular o Substring puede ser inutilizable por int.Parse / double.Parse , entonces use la variante TryParse relevante:

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

No sé cómo hacer eso en LINQ, pero tal vez te guste esta forma de:

 Array.Sort(partNumbers, new AlphanumComparatorFast()); 

// Mostrar los resultados

 foreach (string h in partNumbers ) { Console.WriteLine(h); }