Ordine di ordinamento naturale in C #

Qualcuno ha una buona risorsa o fornisce un campione di un ordinamento naturale in C # per un array FileInfo ? Sto implementando l’interfaccia IComparer nella mia specie.

La cosa più semplice da fare è solo P / Richiamare la funzione integrata in Windows e usarla come funzione di confronto nel tuo IComparer :

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

Michael Kaplan ha alcuni esempi di come funziona questa funzione qui , e le modifiche apportate a Vista per farlo funzionare in modo più intuitivo. Il lato positivo di questa funzione è che avrà lo stesso comportamento della versione di Windows su cui viene eseguito, tuttavia ciò significa che differisce tra le versioni di Windows, quindi è necessario considerare se questo è un problema per te.

Quindi un’implementazione completa sarebbe qualcosa come:

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

Ho pensato di aggiungere a questo (con la soluzione più concisa che ho trovato):

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

Il precedente blocca tutti i numeri nella stringa alla lunghezza massima di tutti i numeri in tutte le stringhe e utilizza la stringa risultante per ordinare.

Il cast to ( int? ) È per consentire raccolte di stringhe senza numeri ( .Max() su un enumerable vuoto lancia una InvalidOperationException ).

Nessuna delle implementazioni esistenti sembrava fantastica, quindi ho scritto la mia. I risultati sono quasi identici all’ordinamento utilizzato dalle moderne versioni di Windows Explorer (Windows 7/8). Le uniche differenze che ho visto sono 1) anche se Windows (per esempio XP) gestisce numeri di qualsiasi lunghezza, ora è limitato a 19 cifre – il mio è illimitato, 2) Windows fornisce risultati incoerenti con determinati set di cifre Unicode – il mio funziona bene (anche se non confronta numericamente cifre da coppie surrogate, né Windows), e 3) il mio non può distinguere diversi tipi di pesi di ordinamento non primari se si verificano in sezioni diverse (ad esempio “e-1é” vs ” é1e- “- le sezioni prima e dopo il numero hanno differenze di peso diacritico e di punteggiatura).

 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 corrisponde al delegato di Comparison :

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

Ecco una class wrapper da utilizzare come 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); } } 

Esempio:

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

Ecco un buon set di nomi di file che utilizzo per i test:

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

Soluzione Pure C # per linq orderby:

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

La mia soluzione:

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

risultati:

 1 a2 a3 a4 a10 b4 b5 b400 C1d c1d2 

La risposta di Matthews Horsley è il metodo più veloce che non modifica il comportamento a seconda della versione di Windows in esecuzione sul tuo programma. Tuttavia, può essere ancora più veloce creando la regex una volta e usando RegexOptions.Compiled. Ho anche aggiunto l’opzione di inserire un comparatore di stringhe in modo da poter ignorare il caso, se necessario, e migliorare la leggibilità un po ‘.

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

Utilizzare per

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

Ciò richiede 450 ms per ordinare 100.000 stringhe rispetto a 300 ms per il confronto di stringa .net predefinito – piuttosto veloce!

Bisogna stare attenti – mi ricordo vagamente di aver letto che StrCmpLogicalW, o qualcosa del genere, non era strettamente transitivo, e ho osservato i metodi di ordinamento di .NET a volte rimanere bloccati in cicli infiniti se la funzione di confronto infrange quella regola.

Un confronto transitivo segnalerà sempre che a

Aggiungendo la risposta di Greg Beech (perché l’ho appena cercato), se vuoi usare questo da Linq puoi usare l’ OrderBy che accetta un IComparer . Per esempio:

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

Questo è il mio codice per ordinare una stringa con caratteri alfa e numerici.

Innanzitutto, questo metodo di estensione:

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

Quindi, è sufficiente utilizzarlo in qualsiasi parte del codice in questo modo:

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

Come funziona ? Sostituendo con zeri:

  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" 

Funziona con numeri multipli:

  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" 

Spero che sia di aiuto.

Ecco un esempio relativamente semplice che non usa P / Invoke ed evita qualsiasi allocazione durante l’esecuzione.

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

Non ignora gli zeri iniziali, quindi 01 arriva dopo 2 .

Test unitario corrispondente:

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

L’ho effettivamente implementato come metodo di estensione su StringComparer modo da poter fare, ad esempio:

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

Il risultante IComparer può essere utilizzato in tutti i posti come OrderBy , OrderByDescending , ThenBy , ThenByDescending , SortedSet , ecc. E puoi facilmente modificare la distinzione tra maiuscole e minuscole, la cultura, ecc.

L’implementazione è abbastanza semplice e dovrebbe funzionare abbastanza bene anche su sequenze di grandi dimensioni.


L’ho anche pubblicato come un piccolo pacchetto NuGet , quindi puoi semplicemente:

 Install-Package NaturalSort.Extension 

Il codice che include i commenti della documentazione XML e la suite di test è disponibile nel repository GitHub di NaturalSort.Extension .


L’intero codice è questo (se non puoi ancora usare il C # 7, installa il pacchetto 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; } } } 

Espandendo un paio delle risposte precedenti e facendo uso di metodi di estensione, ho trovato il seguente che non ha l’avvertenza di una enumerazione enumerabile potenziale, o problemi di prestazioni riguardanti l’uso di più oggetti regex, o chiamando regex inutilmente, che detto, usa ToList (), che può negare i benefici nelle raccolte più grandi.

Il selettore supporta la tipizzazione generica per consentire l’assegnazione di qualsiasi delegato, gli elementi nella raccolta di origine vengono modificati dal selettore, quindi convertiti in stringhe 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); } 

Avevamo bisogno di un tipo naturale per gestire il testo con il seguente schema:

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

Per qualche ragione, quando ho guardato per la prima volta su SO, non ho trovato questo post e ho implementato il nostro. Rispetto ad alcune delle soluzioni presentate qui, sebbene simili nel concetto, potrebbe avere il vantaggio di essere più semplice e più facile da capire. Tuttavia, mentre provavo a considerare i colli di bottiglia delle prestazioni, è ancora un’implementazione molto più lenta rispetto a OrderBy() predefinito.

Ecco il metodo di estensione che implego:

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

L’idea è di dividere le stringhe originali in blocchi di cifre e non cifre ( "\d+|\D+" ). Poiché si tratta di un’attività potenzialmente costosa, viene eseguita una sola volta per voce. Quindi usiamo un comparatore di oggetti comparabili (mi dispiace, non riesco a trovare un modo più corretto per dirlo). Confronta ogni blocco con il blocco corrispondente nell’altra stringa.

Vorrei un feedback su come questo potrebbe essere migliorato e quali sono i principali difetti. Si noti che la manutenibilità è importante per noi a questo punto e attualmente non la stiamo utilizzando in set di dati estremamente ampi.

Se il tuo codice di fine è per il web (ASP.NET ecc.) Allora l’ordinamento naturale può essere raggiunto usando la funzione javascript di localCampare

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

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

Ecco un ingenuo modo LINQ non lineare e privo di righe (preso a prestito da 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"]