Orden de clasificación natural en C #

¿Alguien tiene un buen recurso o proporciona una muestra de ordenación natural en C # para una matriz FileInfo ? Estoy implementando la interfaz IComparer en mi género.

Lo más fácil es simplemente P / Invocar la función incorporada en Windows, y usarla como la función de comparación en su IComparer :

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

Michael Kaplan tiene algunos ejemplos de cómo funciona esta función aquí , y los cambios que se hicieron para Vista para que funcione de manera más intuitiva. El lado positivo de esta función es que tendrá el mismo comportamiento que la versión de Windows en la que se ejecuta, sin embargo, esto significa que difiere entre las versiones de Windows, por lo que debe considerar si esto es un problema para usted.

Entonces, una implementación completa sería algo así como:

 [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 { public int Compare(string a, string b) { return SafeNativeMethods.StrCmpLogicalW(a, b); } } public sealed class NaturalFileInfoNameComparer : IComparer { public int Compare(FileInfo a, FileInfo b) { return SafeNativeMethods.StrCmpLogicalW(a.Name, b.Name); } } 

Solo pensé en agregar algo más a esto (con la solución más concisa que pude encontrar):

 public static IOrderedEnumerable OrderByAlphaNumeric(this IEnumerable source, Func selector) { int max = source .SelectMany(i => Regex.Matches(selector(i), @"\d+").Cast().Select(m => (int?)m.Value.Length)) .Max() ?? 0; return source.OrderBy(i => Regex.Replace(selector(i), @"\d+", m => m.Value.PadLeft(max, '0'))); } 

Lo anterior rellena cualquier número en la cadena con la longitud máxima de todos los números en todas las cadenas y usa la cadena resultante para ordenar.

El lanzamiento a ( int? ) Es para permitir colecciones de cadenas sin ningún número ( .Max() en un enumerable vacío arroja una InvalidOperationException ).

Ninguna de las implementaciones existentes se veía genial, así que escribí la mía. Los resultados son casi idénticos a la clasificación utilizada por las versiones modernas de Windows Explorer (Windows 7/8). Las únicas diferencias que he visto son 1) aunque Windows solía (por ejemplo, XP) manejar números de cualquier longitud, ahora está limitado a 19 dígitos – el mío es ilimitado, 2) Windows da resultados inconsistentes con ciertos conjuntos de dígitos Unicode – mina funciona bien (aunque no compara numéricamente dígitos de pares de sustitución, ni Windows), y 3) el mío no puede distinguir diferentes tipos de ponderaciones de ordenación no primarias si se producen en diferentes secciones (por ejemplo, “e-1é” vs ” é1e- “- las secciones antes y después del número tienen diferencias de peso diacrítico y de puntuación).

 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, eg 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 firma coincide con el delegado de Comparison :

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

Aquí hay una clase de contenedor para usar como IComparer :

 public class CustomComparer : IComparer { private Comparison _comparison; public CustomComparer(Comparison comparison) { _comparison = comparison; } public int Compare(T x, T y) { return _comparison(x, y); } } 

Ejemplo:

 string[] files = Directory.EnumerateFiles(@"C:\") .OrderBy(f => f, new CustomComparer(CompareNatural)) .ToArray(); 

Aquí hay un buen conjunto de nombres de archivo que utilizo para probar:

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

Solución Pure C # para el pedido de linq:

http://zootfroot.blogspot.com/2009/09/natural-sort-compare-with-linq-orderby.html

 public class NaturalSortComparer : IComparer, IDisposable { private bool isAscending; public NaturalSortComparer(bool inAscendingOrder = true) { this.isAscending = inAscendingOrder; } #region IComparer Members public int Compare(string x, string y) { throw new NotImplementedException(); } #endregion #region IComparer Members int IComparer.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 table = new Dictionary(); public void Dispose() { table.Clear(); table = null; } } 

Mi solución:

 void Main() { new[] {"a4","a3","a2","a10","b5","b4","b400","1","C1d","c1d2"}.OrderBy(x => x, new NaturalStringComparer()).Dump(); } public class NaturalStringComparer : IComparer { private static readonly Regex _re = new Regex(@"(?< =\D)(?=\d)|(?<=\d)(?=\D)", RegexOptions.Compiled); public int Compare(string x, string y) { x = x.ToLower(); y = y.ToLower(); if(string.Compare(x, 0, y, 0, Math.Min(x.Length, y.Length)) == 0) { if(x.Length == y.Length) return 0; return x.Length < y.Length ? -1 : 1; } var a = _re.Split(x); var b = _re.Split(y); int i = 0; while(true) { int r = PartCompare(a[i], b[i]); if(r != 0) return r; ++i; } } private static int PartCompare(string x, string y) { int a, b; if(int.TryParse(x, out a) && int.TryParse(y, out b)) return a.CompareTo(b); return x.CompareTo(y); } } 

Resultados:

 1 a2 a3 a4 a10 b4 b5 b400 C1d c1d2 

La respuesta de Matthews Horsleys es el método más rápido que no cambia el comportamiento según la versión de Windows en la que se ejecute el progtwig. Sin embargo, puede ser aún más rápido creando la expresión regular una vez y usando RegexOptions.Compiled. También agregué la opción de insertar un comparador de cadenas para que pueda ignorar el caso si es necesario y mejorar la legibilidad un poco.

  public static IEnumerable OrderByNatural(this IEnumerable items, Func selector, StringComparer stringComparer = null) { var regex = new Regex(@"\d+", RegexOptions.Compiled); int maxDigits = items .SelectMany(i => regex.Matches(selector(i)).Cast().Select(digitChunk => (int?)digitChunk.Value.Length)) .Max() ?? 0; return items.OrderBy(i => regex.Replace(selector(i), match => match.Value.PadLeft(maxDigits, '0')), stringComparer ?? StringComparer.CurrentCulture); } 

Usar por

 var sortedEmployees = employees.OrderByNatural(emp => emp.Name); 

Esto requiere 450ms para ordenar 100.000 cadenas en comparación con 300ms para la comparación de cadenas por defecto de .net, ¡bastante rápido!

Debe tener cuidado: recuerdo vagamente haber leído que StrCmpLogicalW, o algo así, no era estrictamente transitivo, y he observado que los métodos de ordenación de .NET a veces se atascan en bucles infinitos si la función de comparación rompe esa regla.

Una comparación transitiva siempre informará que a

Agregando a la respuesta de Greg Beech (porque recién he estado buscando), si quieres usar esto desde Linq puedes usar el OrderBy que toma un IComparer . P.ej:

 var items = new List(); // fill items var sorted = items.OrderBy(item => item.Name, new NaturalStringComparer()); 

Este es mi código para ordenar una cadena con caracteres alfabéticos y numéricos.

Primero, este método de extensión:

 public static IEnumerable AlphanumericSort(this IEnumerable me) { return me.OrderBy(x => Regex.Replace(x, @"\d+", m => m.Value.PadLeft(50, '0'))); } 

Entonces, simplemente úsalo en cualquier parte de tu código como este:

 List test = new List() { "The 1st", "The 12th", "The 2nd" }; test = test.AlphanumericSort(); 

Cómo funciona ? Al reemplazar con ceros:

  Original | Regex Replace | The | Returned List | Apply PadLeft | Sorting | List | | | "The 1st" | "The 001st" | "The 001st" | "The 1st" "The 12th" | "The 012th" | "The 002nd" | "The 2nd" "The 2nd" | "The 002nd" | "The 012th" | "The 12th" 

Funciona con números múltiples:

  Alphabetical Sorting | Alphanumeric Sorting | "Page 21, Line 42" | "Page 3, Line 7" "Page 21, Line 5" | "Page 3, Line 32" "Page 3, Line 32" | "Page 21, Line 5" "Page 3, Line 7" | "Page 21, Line 42" 

Espero que sea de ayuda.

Aquí hay un ejemplo relativamente simple que no usa P / Invoke y evita cualquier asignación durante la ejecución.

 internal sealed class NumericStringComparer : IComparer { public static NumericStringComparer Instance { get; } = new NumericStringComparer(); public int Compare(string x, string y) { // sort nulls to the start if (x == null) return y == null ? 0 : -1; if (y == null) return 1; var ix = 0; var iy = 0; while (true) { // sort shorter strings to the start if (ix >= x.Length) return iy >= y.Length ? 0 : -1; if (iy >= y.Length) return 1; var cx = x[ix]; var cy = y[iy]; int result; if (char.IsDigit(cx) && char.IsDigit(cy)) result = CompareInteger(x, y, ref ix, ref iy); else result = cx.CompareTo(y[iy]); if (result != 0) return result; ix++; iy++; } } private static int CompareInteger(string x, string y, ref int ix, ref int iy) { var lx = GetNumLength(x, ix); var ly = GetNumLength(y, iy); // shorter number first (note, doesn't handle leading zeroes) if (lx != ly) return lx.CompareTo(ly); for (var i = 0; i < lx; i++) { var result = x[ix++].CompareTo(y[iy++]); if (result != 0) return result; } return 0; } private static int GetNumLength(string s, int i) { var length = 0; while (i < s.Length && char.IsDigit(s[i++])) length++; return length; } } 

No ignora los ceros a la izquierda, por lo que 01 viene después de 2 .

Prueba unitaria correspondiente:

 public class NumericStringComparerTests { [Fact] public void OrdersCorrectly() { AssertEqual("", ""); AssertEqual(null, null); AssertEqual("Hello", "Hello"); AssertEqual("Hello123", "Hello123"); AssertEqual("123", "123"); AssertEqual("123Hello", "123Hello"); AssertOrdered("", "Hello"); AssertOrdered(null, "Hello"); AssertOrdered("Hello", "Hello1"); AssertOrdered("Hello123", "Hello124"); AssertOrdered("Hello123", "Hello133"); AssertOrdered("Hello123", "Hello223"); AssertOrdered("123", "124"); AssertOrdered("123", "133"); AssertOrdered("123", "223"); AssertOrdered("123", "1234"); AssertOrdered("123", "2345"); AssertOrdered("0", "1"); AssertOrdered("123Hello", "124Hello"); AssertOrdered("123Hello", "133Hello"); AssertOrdered("123Hello", "223Hello"); AssertOrdered("123Hello", "1234Hello"); } private static void AssertEqual(string x, string y) { Assert.Equal(0, NumericStringComparer.Instance.Compare(x, y)); Assert.Equal(0, NumericStringComparer.Instance.Compare(y, x)); } private static void AssertOrdered(string x, string y) { Assert.Equal(-1, NumericStringComparer.Instance.Compare(x, y)); Assert.Equal( 1, NumericStringComparer.Instance.Compare(y, x)); } } 

De hecho, lo he implementado como un método de extensión en StringComparer para que pueda hacer, por ejemplo:

  • StringComparer.CurrentCulture.WithNaturalSort() o
  • StringComparer.OrdinalIgnoreCase.WithNaturalSort() .

El IComparer resultante se puede usar en todos los lugares, como OrderBy , OrderByDescending , ThenBy , ThenByDescending , SortedSet , etc. Y todavía puede modificar fácilmente la distinción entre mayúsculas y minúsculas, la cultura, etc.

La implementación es bastante trivial y debería funcionar bastante bien incluso en secuencias grandes.


También lo publiqué como un pequeño paquete NuGet , así que puedes hacer lo siguiente:

 Install-Package NaturalSort.Extension 

El código que incluye los comentarios de la documentación XML y el conjunto de pruebas está disponible en el repository NaturalSort.Extension GitHub .


El código completo es esto (si aún no puede usar C # 7, simplemente instale el paquete NuGet):

 public static class StringComparerNaturalSortExtension { public static IComparer WithNaturalSort(this StringComparer stringComparer) => new NaturalSortComparer(stringComparer); private class NaturalSortComparer : IComparer { public NaturalSortComparer(StringComparer stringComparer) { _stringComparer = stringComparer; } private readonly StringComparer _stringComparer; private static readonly Regex NumberSequenceRegex = new Regex(@"(\d+)", RegexOptions.Compiled | RegexOptions.CultureInvariant); private static string[] Tokenize(string s) => s == null ? new string[] { } : NumberSequenceRegex.Split(s); private static ulong ParseNumberOrZero(string s) => ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var result) ? result : 0; public int Compare(string s1, string s2) { var tokens1 = Tokenize(s1); var tokens2 = Tokenize(s2); var zipCompare = tokens1.Zip(tokens2, TokenCompare).FirstOrDefault(x => x != 0); if (zipCompare != 0) return zipCompare; var lengthCompare = tokens1.Length.CompareTo(tokens2.Length); return lengthCompare; } private int TokenCompare(string token1, string token2) { var number1 = ParseNumberOrZero(token1); var number2 = ParseNumberOrZero(token2); var numberCompare = number1.CompareTo(number2); if (numberCompare != 0) return numberCompare; var stringCompare = _stringComparer.Compare(token1, token2); return stringCompare; } } } 

Ampliando un par de las respuestas anteriores y haciendo uso de los métodos de extensión, se me ocurrió lo siguiente que no tiene las salvedades de una posible enumeración enumerable múltiple, o problemas de rendimiento relacionados con el uso de varios objetos regex, o llamando a regex innecesariamente, que Dicho esto, sí utiliza ToList (), que puede anular los beneficios en colecciones más grandes.

El selector admite el tipado genérico para permitir que se asigne cualquier delegado, los elementos de la colección de origen son mutados por el selector y luego convertidos a cadenas con ToString ().

  private static readonly Regex _NaturalOrderExpr = new Regex(@"\d+", RegexOptions.Compiled); public static IEnumerable OrderByNatural( this IEnumerable source, Func selector) { int max = 0; var selection = source.Select( o => { var v = selector(o); var s = v != null ? v.ToString() : String.Empty; if (!String.IsNullOrWhiteSpace(s)) { var mc = _NaturalOrderExpr.Matches(s); if (mc.Count > 0) { max = Math.Max(max, mc.Cast().Max(m => m.Value.Length)); } } return new { Key = o, Value = s }; }).ToList(); return selection.OrderBy( o => String.IsNullOrWhiteSpace(o.Value) ? o.Value : _NaturalOrderExpr.Replace(o.Value, m => m.Value.PadLeft(max, '0'))) .Select(o => o.Key); } public static IEnumerable OrderByDescendingNatural( this IEnumerable source, Func selector) { int max = 0; var selection = source.Select( o => { var v = selector(o); var s = v != null ? v.ToString() : String.Empty; if (!String.IsNullOrWhiteSpace(s)) { var mc = _NaturalOrderExpr.Matches(s); if (mc.Count > 0) { max = Math.Max(max, mc.Cast().Max(m => m.Value.Length)); } } return new { Key = o, Value = s }; }).ToList(); return selection.OrderByDescending( o => String.IsNullOrWhiteSpace(o.Value) ? o.Value : _NaturalOrderExpr.Replace(o.Value, m => m.Value.PadLeft(max, '0'))) .Select(o => o.Key); } 

Necesitamos un tipo natural para tratar el texto con el siguiente patrón:

 "Test 1-1-1 something" "Test 1-2-3 something" ... 

Por alguna razón, cuando miré por primera vez SO, no encontré esta publicación e implementé la nuestra. En comparación con algunas de las soluciones presentadas aquí, aunque es similar en concepto, podría tener el beneficio de ser tal vez más simple y fácil de entender. Sin embargo, si bien OrderBy() analizar los cuellos de botella de rendimiento, sigue siendo una implementación mucho más lenta que el OrderBy() predeterminado.

Aquí está el método de extensión que implemento:

 public static class EnumerableExtensions { // set up the regex parser once and for all private static readonly Regex Regex = new Regex(@"\d+|\D+", RegexOptions.Compiled | RegexOptions.Singleline); // stateless comparer can be built once private static readonly AggregateComparer Comparer = new AggregateComparer(); public static IEnumerable OrderByNatural(this IEnumerable source, Func selector) { // first extract string from object using selector // then extract digit and non-digit groups Func> splitter = s => Regex.Matches(selector(s)) .Cast() .Select(m => Char.IsDigit(m.Value[0]) ? (IComparable) int.Parse(m.Value) : m.Value); return source.OrderBy(splitter, Comparer); } ///  /// This comparer will compare two lists of objects against each other ///  /// Objects in each list are compare to their corresponding elements in the other /// list until a difference is found. private class AggregateComparer : IComparer> { public int Compare(IEnumerable x, IEnumerable y) { return x.Zip(y, (a, b) => new {a, b}) // walk both lists .Select(pair => pair.a.CompareTo(pair.b)) // compare each object .FirstOrDefault(result => result != 0); // until a difference is found } } } 

La idea es dividir las cadenas originales en bloques de dígitos y no dígitos ( "\d+|\D+" ). Como esta es una tarea potencialmente costosa, solo se realiza una vez por entrada. Luego usamos un comparador de objetos comparables (lo siento, no puedo encontrar una forma más adecuada de decirlo). Compara cada bloque con su bloque correspondiente en la otra cadena.

Me gustaría recibir comentarios sobre cómo se podría mejorar esto y cuáles son los principales defectos. Tenga en cuenta que la capacidad de mantenimiento es importante para nosotros en este momento y que actualmente no estamos usando esto en conjuntos de datos extremadamente grandes.

Si su código de finalización es para web (ASP.NET, etc.), la clasificación natural se puede lograr utilizando la función javascript localCampare.

 '10'.localeCompare('2', undefined, {numeric: true, sensitivity: 'base'}) 

https://stackoverflow.com/a/38641281/952018

Aquí hay una ingenua línea de LINQ de una línea (prestada de Python):

 List alphaStrings = new List() { "10","2","3","4","50","11","100","a12","b12" }; alphaStrings.OrderBy(g => new Tuple(g.ToCharArray().All(char.IsDigit)? int.Parse(g) : int.MaxValue, g)).Dump(); // Order Now: ["2","3","4","10","11","50","100","a12","b12"]