Classificação alfanumérica usando o LINQ

Eu tenho uma string[] na qual todos os elementos terminam com algum valor numérico.

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

Eu estou tentando classificar a matriz acima da seguinte maneira usando LINQ mas eu não estou recebendo o resultado esperado.

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

Resultado atual:

AB1
Ab11
AB2
ABC1
ABC10
ABC10
ABC11
ABC2

resultado esperado

AB1
AB2
AB11
..

Isso ocorre porque a ordem padrão para string é a ordenação padrão por dictionary alfanumérico (lexicográfico), e ABC11 virá antes de ABC2 porque o pedido sempre procede da esquerda para a direita.

Para obter o que você quer, você precisa preencher a parte numérica em sua ordem por cláusula, algo como:

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

onde os PadNumbers podem ser definidos como:

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

Isso preenche zeros para qualquer número (ou números) que aparecem na string de input, de modo que OrderBy veja:

 ABC0000000010 ABC0000000001 ... AB0000000011 

O preenchimento só acontece na chave usada para comparação. As strings originais (sem preenchimento) são preservadas no resultado.

Observe que essa abordagem pressupõe um número máximo de dígitos para números na input.

Uma implementação adequada de um método de sorting alfanumérica que “simplesmente funciona” pode ser encontrada no site de Dave Koelle . A versão C # está aqui .

 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 desta Classe:

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

Se você quiser ordenar uma lista de objects por uma propriedade específica usando LINQ e um comparador customizado como o de Dave Koelle, você faria algo assim:

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

Você também tem que alterar a class de Dave para herdar de System.Collections.Generic.IComparer vez do IComparer básico, para que a assinatura da class se torne:

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

Pessoalmente, prefiro a implementação de James McCormack porque ele implementa o IDisposable, embora meu benchmarking mostre que ele é um pouco mais lento.

Você pode usar o PInvoke para obter resultados rápidos e bons:

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

Você pode usá-lo como AlphanumComparatorFast da resposta acima.

Você pode PInvoke para StrCmpLogicalW (a function do Windows) para fazer isso. Veja aqui: Ordem de Classificação Natural em C #

Bem, parece que ele está fazendo uma ordenação Lexicográfica, independentemente de caracteres pequenos ou maiúsculos.

Você pode tentar usar alguma expressão personalizada nesse lambda para fazer isso.

Não há nenhuma maneira natural de fazer isso no .net, mas dê uma olhada neste post sobre triagem natural

Você poderia colocar isso em um método de extensão e usá-lo em vez de OrderBy

Como o número de caracteres no início é variável, uma expressão regular ajudaria:

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

Se houvesse um número fixo de caracteres de prefixo, você poderia usar o método Substring para extrair a partir dos caracteres relevantes:

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

Se os números puderem conter um separador decimal ou um separador de milhares, a expressão regular também deverá permitir esses caracteres:

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

Se a seqüência de caracteres retornada pela expressão regular ou Substring pode ser incomparável por int.Parse / double.Parse , use a 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; }); 

Eu não sei como fazer isso no LINQ, mas talvez você goste desta maneira de:

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

// Exibe os resultados

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