Natural Sort Order in C# - c#
Anyone have a good resource or provide a sample of a natural order sort in C# for an FileInfo array? I am implementing the IComparer interface in my sorts.
The easiest thing to do is just P/Invoke the built-in function in Windows, and use it as the comparison function in your IComparer:
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern int StrCmpLogicalW(string psz1, string psz2);
Michael Kaplan has some examples of how this function works here, and the changes that were made for Vista to make it work more intuitively. The plus side of this function is that it will have the same behaviour as the version of Windows it runs on, however this does mean that it differs between versions of Windows so you need to consider whether this is a problem for you.
So a complete implementation would be something like:
[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<string>
{
public int Compare(string a, string b)
{
return SafeNativeMethods.StrCmpLogicalW(a, b);
}
}
public sealed class NaturalFileInfoNameComparer : IComparer<FileInfo>
{
public int Compare(FileInfo a, FileInfo b)
{
return SafeNativeMethods.StrCmpLogicalW(a.Name, b.Name);
}
}
Just thought I'd add to this (with the most concise solution I could find):
public static IOrderedEnumerable<T> OrderByAlphaNumeric<T>(this IEnumerable<T> source, Func<T, string> selector)
{
int max = source
.SelectMany(i => Regex.Matches(selector(i), #"\d+").Cast<Match>().Select(m => (int?)m.Value.Length))
.Max() ?? 0;
return source.OrderBy(i => Regex.Replace(selector(i), #"\d+", m => m.Value.PadLeft(max, '0')));
}
The above pads any numbers in the string to the max length of all numbers in all strings and uses the resulting string to sort.
The cast to (int?) is to allow for collections of strings without any numbers (.Max() on an empty enumerable throws an InvalidOperationException).
None of the existing implementations looked great so I wrote my own. The results are almost identical to the sorting used by modern versions of Windows Explorer (Windows 7/8). The only differences I've seen are 1) although Windows used to (e.g. XP) handle numbers of any length, it's now limited to 19 digits - mine is unlimited, 2) Windows gives inconsistent results with certain sets of Unicode digits - mine works fine (although it doesn't numerically compare digits from surrogate pairs; nor does Windows), and 3) mine can't distinguish different types of non-primary sort weights if they occur in different sections (e.g. "e-1é" vs "é1e-" - the sections before and after the number have diacritic and punctuation weight differences).
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, e.g. 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;
}
The signature matches the Comparison<string> delegate:
string[] files = Directory.GetFiles(#"C:\");
Array.Sort(files, CompareNatural);
Here's a wrapper class for use as IComparer<string>:
public class CustomComparer<T> : IComparer<T> {
private Comparison<T> _comparison;
public CustomComparer(Comparison<T> comparison) {
_comparison = comparison;
}
public int Compare(T x, T y) {
return _comparison(x, y);
}
}
Example:
string[] files = Directory.EnumerateFiles(#"C:\")
.OrderBy(f => f, new CustomComparer<string>(CompareNatural))
.ToArray();
Here's a good set of filenames I use for testing:
Func<string, string> 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();
Matthews Horsleys answer is the fastest method which doesn't change behaviour depending on which version of windows your program is running on. However, it can be even faster by creating the regex once, and using RegexOptions.Compiled. I also added the option of inserting a string comparer so you can ignore case if needed, and improved readability a bit.
public static IEnumerable<T> OrderByNatural<T>(this IEnumerable<T> items, Func<T, string> selector, StringComparer stringComparer = null)
{
var regex = new Regex(#"\d+", RegexOptions.Compiled);
int maxDigits = items
.SelectMany(i => regex.Matches(selector(i)).Cast<Match>().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);
}
Use by
var sortedEmployees = employees.OrderByNatural(emp => emp.Name);
This takes 450ms to sort 100,000 strings compared to 300ms for the default .net string comparison - pretty fast!
Pure C# solution for linq orderby:
http://zootfroot.blogspot.com/2009/09/natural-sort-compare-with-linq-orderby.html
public class NaturalSortComparer<T> : IComparer<string>, IDisposable
{
private bool isAscending;
public NaturalSortComparer(bool inAscendingOrder = true)
{
this.isAscending = inAscendingOrder;
}
#region IComparer<string> Members
public int Compare(string x, string y)
{
throw new NotImplementedException();
}
#endregion
#region IComparer<string> Members
int IComparer<string>.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<string, string[]> table = new Dictionary<string, string[]>();
public void Dispose()
{
table.Clear();
table = null;
}
}
My solution:
void Main()
{
new[] {"a4","a3","a2","a10","b5","b4","b400","1","C1d","c1d2"}.OrderBy(x => x, new NaturalStringComparer()).Dump();
}
public class NaturalStringComparer : IComparer<string>
{
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);
}
}
Results:
1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2
You do need to be careful -- I vaguely recall reading that StrCmpLogicalW, or something like it, was not strictly transitive, and I have observed .NET's sort methods to sometimes get stuck in infinite loops if the comparison function breaks that rule.
A transitive comparison will always report that a < c if a < b and b < c. There exists a function that does a natural sort order comparison that does not always meet that criterion, but I can't recall whether it is StrCmpLogicalW or something else.
This is my code to sort a string having both alpha and numeric characters.
First, this extension method:
public static IEnumerable<string> AlphanumericSort(this IEnumerable<string> me)
{
return me.OrderBy(x => Regex.Replace(x, #"\d+", m => m.Value.PadLeft(50, '0')));
}
Then, simply use it anywhere in your code like this:
List<string> test = new List<string>() { "The 1st", "The 12th", "The 2nd" };
test = test.AlphanumericSort();
How does it works ? By replaceing with zeros:
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"
Works with multiples numbers:
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"
Hope that's will help.
Here's a version for .NET Core 2.1+ / .NET 5.0+, using spans to avoid allocations
public class NaturalSortStringComparer : IComparer<string>
{
public static NaturalSortStringComparer Ordinal { get; } = new NaturalSortStringComparer(StringComparison.Ordinal);
public static NaturalSortStringComparer OrdinalIgnoreCase { get; } = new NaturalSortStringComparer(StringComparison.OrdinalIgnoreCase);
public static NaturalSortStringComparer CurrentCulture { get; } = new NaturalSortStringComparer(StringComparison.CurrentCulture);
public static NaturalSortStringComparer CurrentCultureIgnoreCase { get; } = new NaturalSortStringComparer(StringComparison.CurrentCultureIgnoreCase);
public static NaturalSortStringComparer InvariantCulture { get; } = new NaturalSortStringComparer(StringComparison.InvariantCulture);
public static NaturalSortStringComparer InvariantCultureIgnoreCase { get; } = new NaturalSortStringComparer(StringComparison.InvariantCultureIgnoreCase);
private readonly StringComparison _comparison;
public NaturalSortStringComparer(StringComparison comparison)
{
_comparison = comparison;
}
public int Compare(string x, string y)
{
// Let string.Compare handle the case where x or y is null
if (x is null || y is null)
return string.Compare(x, y, _comparison);
var xSegments = GetSegments(x);
var ySegments = GetSegments(y);
while (xSegments.MoveNext() && ySegments.MoveNext())
{
int cmp;
// If they're both numbers, compare the value
if (xSegments.CurrentIsNumber && ySegments.CurrentIsNumber)
{
var xValue = long.Parse(xSegments.Current);
var yValue = long.Parse(ySegments.Current);
cmp = xValue.CompareTo(yValue);
if (cmp != 0)
return cmp;
}
// If x is a number and y is not, x is "lesser than" y
else if (xSegments.CurrentIsNumber)
{
return -1;
}
// If y is a number and x is not, x is "greater than" y
else if (ySegments.CurrentIsNumber)
{
return 1;
}
// OK, neither are number, compare the segments as text
cmp = xSegments.Current.CompareTo(ySegments.Current, _comparison);
if (cmp != 0)
return cmp;
}
// At this point, either all segments are equal, or one string is shorter than the other
// If x is shorter, it's "lesser than" y
if (x.Length < y.Length)
return -1;
// If x is longer, it's "greater than" y
if (x.Length > y.Length)
return 1;
// If they have the same length, they're equal
return 0;
}
private static StringSegmentEnumerator GetSegments(string s) => new StringSegmentEnumerator(s);
private struct StringSegmentEnumerator
{
private readonly string _s;
private int _start;
private int _length;
public StringSegmentEnumerator(string s)
{
_s = s;
_start = -1;
_length = 0;
CurrentIsNumber = false;
}
public ReadOnlySpan<char> Current => _s.AsSpan(_start, _length);
public bool CurrentIsNumber { get; private set; }
public bool MoveNext()
{
var currentPosition = _start >= 0
? _start + _length
: 0;
if (currentPosition >= _s.Length)
return false;
int start = currentPosition;
bool isFirstCharDigit = Char.IsDigit(_s[currentPosition]);
while (++currentPosition < _s.Length && Char.IsDigit(_s[currentPosition]) == isFirstCharDigit)
{
}
_start = start;
_length = currentPosition - start;
CurrentIsNumber = isFirstCharDigit;
return true;
}
}
}
Adding to Greg Beech's answer (because I've just been searching for that), if you want to use this from Linq you can use the OrderBy that takes an IComparer. E.g.:
var items = new List<MyItem>();
// fill items
var sorted = items.OrderBy(item => item.Name, new NaturalStringComparer());
Here's a relatively simple example that doesn't use P/Invoke and avoids any allocation during execution.
Feel free to use the code from here, or if it's easier there's a NuGet package:
https://www.nuget.org/packages/NaturalSort
https://github.com/drewnoakes/natural-sort
internal sealed class NaturalStringComparer : IComparer<string>
{
public static NaturalStringComparer Instance { get; } = new NaturalStringComparer();
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;
}
}
It doesn't ignore leading zeroes, so 01 comes after 2.
Corresponding unit test:
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, NaturalStringComparer.Instance.Compare(x, y));
Assert.Equal(0, NaturalStringComparer.Instance.Compare(y, x));
}
private static void AssertOrdered(string x, string y)
{
Assert.Equal(-1, NaturalStringComparer.Instance.Compare(x, y));
Assert.Equal( 1, NaturalStringComparer.Instance.Compare(y, x));
}
}
I've actually implemented it as an extension method on the StringComparer so that you could do for example:
StringComparer.CurrentCulture.WithNaturalSort() or
StringComparer.OrdinalIgnoreCase.WithNaturalSort().
The resulting IComparer<string> can be used in all places like OrderBy, OrderByDescending, ThenBy, ThenByDescending, SortedSet<string>, etc. And you can still easily tweak case sensitivity, culture, etc.
The implementation is fairly trivial and it should perform quite well even on large sequences.
I've also published it as a tiny NuGet package, so you can just do:
Install-Package NaturalSort.Extension
The code including XML documentation comments and suite of tests is available in the NaturalSort.Extension GitHub repository.
The entire code is this (if you cannot use C# 7 yet, just install the NuGet package):
public static class StringComparerNaturalSortExtension
{
public static IComparer<string> WithNaturalSort(this StringComparer stringComparer) => new NaturalSortComparer(stringComparer);
private class NaturalSortComparer : IComparer<string>
{
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;
}
}
}
Inspired by Michael Parker's solution, here is an IComparer implementation that you can drop in to any of the linq ordering methods:
private class NaturalStringComparer : IComparer<string>
{
public int Compare(string left, string right)
{
int max = new[] { left, right }
.SelectMany(x => Regex.Matches(x, #"\d+").Cast<Match>().Select(y => (int?)y.Value.Length))
.Max() ?? 0;
var leftPadded = Regex.Replace(left, #"\d+", m => m.Value.PadLeft(max, '0'));
var rightPadded = Regex.Replace(right, #"\d+", m => m.Value.PadLeft(max, '0'));
return string.Compare(leftPadded, rightPadded);
}
}
Here is a naive one-line regex-less LINQ way (borrowed from python):
var alphaStrings = new List<string>() { "10","2","3","4","50","11","100","a12","b12" };
var orderedString = alphaStrings.OrderBy(g => new Tuple<int, string>(g.ToCharArray().All(char.IsDigit)? int.Parse(g) : int.MaxValue, g));
// Order Now: ["2","3","4","10","11","50","100","a12","b12"]
Expanding on a couple of the previous answers and making use of extension methods, I came up with the following that doesn't have the caveats of potential multiple enumerable enumeration, or performance issues concerned with using multiple regex objects, or calling regex needlessly, that being said, it does use ToList(), which can negate the benefits in larger collections.
The selector supports generic typing to allow any delegate to be assigned, the elements in the source collection are mutated by the selector, then converted to strings with ToString().
private static readonly Regex _NaturalOrderExpr = new Regex(#"\d+", RegexOptions.Compiled);
public static IEnumerable<TSource> OrderByNatural<TSource, TKey>(
this IEnumerable<TSource> source, Func<TSource, TKey> 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<Match>().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<TSource> OrderByDescendingNatural<TSource, TKey>(
this IEnumerable<TSource> source, Func<TSource, TKey> 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<Match>().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);
}
A version that's easier to read/maintain.
public class NaturalStringComparer : IComparer<string>
{
public static NaturalStringComparer Instance { get; } = new NaturalStringComparer();
public int Compare(string x, string y) {
const int LeftIsSmaller = -1;
const int RightIsSmaller = 1;
const int Equal = 0;
var leftString = x;
var rightString = y;
var stringComparer = CultureInfo.CurrentCulture.CompareInfo;
int rightIndex;
int leftIndex;
for (leftIndex = 0, rightIndex = 0;
leftIndex < leftString.Length && rightIndex < rightString.Length;
leftIndex++, rightIndex++) {
var leftChar = leftString[leftIndex];
var rightChar = rightString[leftIndex];
var leftIsNumber = char.IsNumber(leftChar);
var rightIsNumber = char.IsNumber(rightChar);
if (!leftIsNumber && !rightIsNumber) {
var result = stringComparer.Compare(leftString, leftIndex, 1, rightString, leftIndex, 1);
if (result != 0) return result;
} else if (leftIsNumber && !rightIsNumber) {
return LeftIsSmaller;
} else if (!leftIsNumber && rightIsNumber) {
return RightIsSmaller;
} else {
var leftNumberLength = NumberLength(leftString, leftIndex, out var leftNumber);
var rightNumberLength = NumberLength(rightString, rightIndex, out var rightNumber);
if (leftNumberLength < rightNumberLength) {
return LeftIsSmaller;
} else if (leftNumberLength > rightNumberLength) {
return RightIsSmaller;
} else {
if(leftNumber < rightNumber) {
return LeftIsSmaller;
} else if(leftNumber > rightNumber) {
return RightIsSmaller;
}
}
}
}
if (leftString.Length < rightString.Length) {
return LeftIsSmaller;
} else if(leftString.Length > rightString.Length) {
return RightIsSmaller;
}
return Equal;
}
public int NumberLength(string str, int offset, out int number) {
if (string.IsNullOrWhiteSpace(str)) throw new ArgumentNullException(nameof(str));
if (offset >= str.Length) throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be less than the length of the string.");
var currentOffset = offset;
var curChar = str[currentOffset];
if (!char.IsNumber(curChar))
throw new ArgumentException($"'{curChar}' is not a number.", nameof(offset));
int length = 1;
var numberString = string.Empty;
for (currentOffset = offset + 1;
currentOffset < str.Length;
currentOffset++, length++) {
curChar = str[currentOffset];
numberString += curChar;
if (!char.IsNumber(curChar)) {
number = int.Parse(numberString);
return length;
}
}
number = int.Parse(numberString);
return length;
}
}
We had a need for a natural sort to deal with text with the following pattern:
"Test 1-1-1 something"
"Test 1-2-3 something"
...
For some reason when I first looked on SO, I didn't find this post and implemented our own. Compared to some of the solutions presented here, while similar in concept, it could have the benefit of maybe being simpler and easier to understand. However, while I did try to look at performance bottlenecks, It is still a much slower implementation than the default OrderBy().
Here is the extension method I implement:
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<T> OrderByNatural<T>(this IEnumerable<T> source, Func<T, string> selector)
{
// first extract string from object using selector
// then extract digit and non-digit groups
Func<T, IEnumerable<IComparable>> splitter =
s => Regex.Matches(selector(s))
.Cast<Match>()
.Select(m => Char.IsDigit(m.Value[0]) ? (IComparable) int.Parse(m.Value) : m.Value);
return source.OrderBy(splitter, Comparer);
}
/// <summary>
/// This comparer will compare two lists of objects against each other
/// </summary>
/// <remarks>Objects in each list are compare to their corresponding elements in the other
/// list until a difference is found.</remarks>
private class AggregateComparer : IComparer<IEnumerable<IComparable>>
{
public int Compare(IEnumerable<IComparable> x, IEnumerable<IComparable> 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
}
}
}
The idea is to split the original strings into blocks of digits and non-digits ("\d+|\D+"). Since this is a potentially expensive task, it is done only once per entry. We then use a comparer of comparable objects (sorry, I can't find a more proper way to say it). It compares each block to its corresponding block in the other string.
I would like feedback on how this could be improved and what the major flaws are. Note that maintainability is important to us at this point and we are not currently using this in extremely large data sets.
Let me explain my problem and how i was able to solve it.
Problem:- Sort files based on FileName from FileInfo objects which are retrieved from a Directory.
Solution:- I selected the file names from FileInfo and trimed the ".png" part of the file name. Now, just do List.Sort(), which sorts the filenames in Natural sorting order. Based on my testing i found that having .png messes up sorting order. Have a look at the below code
var imageNameList = new DirectoryInfo(#"C:\Temp\Images").GetFiles("*.png").Select(x =>x.Name.Substring(0, x.Name.Length - 4)).ToList();
imageNameList.Sort();
Related
Index out of bounds of array but only sometimes [duplicate]
Suppose I had a string: string str = "1111222233334444"; How can I break this string into chunks of some size? e.g., breaking this into sizes of 4 would return strings: "1111" "2222" "3333" "4444"
static IEnumerable<string> Split(string str, int chunkSize) { return Enumerable.Range(0, str.Length / chunkSize) .Select(i => str.Substring(i * chunkSize, chunkSize)); } Please note that additional code might be required to gracefully handle edge cases (null or empty input string, chunkSize == 0, input string length not divisible by chunkSize, etc.). The original question doesn't specify any requirements for these edge cases and in real life the requirements might vary so they are out of scope of this answer.
In a combination of dove+Konstatin's answers... static IEnumerable<string> WholeChunks(string str, int chunkSize) { for (int i = 0; i < str.Length; i += chunkSize) yield return str.Substring(i, chunkSize); } This will work for all strings that can be split into a whole number of chunks, and will throw an exception otherwise. If you want to support strings of any length you could use the following code: static IEnumerable<string> ChunksUpto(string str, int maxChunkSize) { for (int i = 0; i < str.Length; i += maxChunkSize) yield return str.Substring(i, Math.Min(maxChunkSize, str.Length-i)); } However, the the OP explicitly stated he does not need this; it's somewhat longer and harder to read, slightly slower. In the spirit of KISS and YAGNI, I'd go with the first option: it's probably the most efficient implementation possible, and it's very short, readable, and, importantly, throws an exception for nonconforming input.
Why not loops? Here's something that would do it quite well: string str = "111122223333444455"; int chunkSize = 4; int stringLength = str.Length; for (int i = 0; i < stringLength ; i += chunkSize) { if (i + chunkSize > stringLength) chunkSize = stringLength - i; Console.WriteLine(str.Substring(i, chunkSize)); } Console.ReadLine(); I don't know how you'd deal with case where the string is not factor of 4, but not saying you're idea is not possible, just wondering the motivation for it if a simple for loop does it very well? Obviously the above could be cleaned and even put in as an extension method. Or as mentioned in comments, you know it's /4 then str = "1111222233334444"; for (int i = 0; i < stringLength; i += chunkSize) {Console.WriteLine(str.Substring(i, chunkSize));}
This is based on #dove solution but implemented as an extension method. Benefits: Extension method Covers corner cases Splits string with any chars: numbers, letters, other symbols Code public static class EnumerableEx { public static IEnumerable<string> SplitBy(this string str, int chunkLength) { if (String.IsNullOrEmpty(str)) throw new ArgumentException(); if (chunkLength < 1) throw new ArgumentException(); for (int i = 0; i < str.Length; i += chunkLength) { if (chunkLength + i > str.Length) chunkLength = str.Length - i; yield return str.Substring(i, chunkLength); } } } Usage var result = "bobjoecat".SplitBy(3); // bob, joe, cat Unit tests removed for brevity (see previous revision)
Using regular expressions and Linq: List<string> groups = (from Match m in Regex.Matches(str, #"\d{4}") select m.Value).ToList(); I find this to be more readable, but it's just a personal opinion. It can also be a one-liner : ).
How's this for a one-liner? List<string> result = new List<string>(Regex.Split(target, #"(?<=\G.{4})", RegexOptions.Singleline)); With this regex it doesn't matter if the last chunk is less than four characters, because it only ever looks at the characters behind it. I'm sure this isn't the most efficient solution, but I just had to toss it out there.
Starting with .NET 6, we can also use the Chunk method: var result = str .Chunk(4) .Select(x => new string(x)) .ToList();
I recently had to write something that accomplishes this at work, so I thought I would post my solution to this problem. As an added bonus, the functionality of this solution provides a way to split the string in the opposite direction and it does correctly handle unicode characters as previously mentioned by Marvin Pinto above. So, here it is: using System; using Extensions; namespace TestCSharp { class Program { static void Main(string[] args) { string asciiStr = "This is a string."; string unicodeStr = "これは文字列です。"; string[] array1 = asciiStr.Split(4); string[] array2 = asciiStr.Split(-4); string[] array3 = asciiStr.Split(7); string[] array4 = asciiStr.Split(-7); string[] array5 = unicodeStr.Split(5); string[] array6 = unicodeStr.Split(-5); } } } namespace Extensions { public static class StringExtensions { /// <summary>Returns a string array that contains the substrings in this string that are seperated a given fixed length.</summary> /// <param name="s">This string object.</param> /// <param name="length">Size of each substring. /// <para>CASE: length > 0 , RESULT: String is split from left to right.</para> /// <para>CASE: length == 0 , RESULT: String is returned as the only entry in the array.</para> /// <para>CASE: length < 0 , RESULT: String is split from right to left.</para> /// </param> /// <returns>String array that has been split into substrings of equal length.</returns> /// <example> /// <code> /// string s = "1234567890"; /// string[] a = s.Split(4); // a == { "1234", "5678", "90" } /// </code> /// </example> public static string[] Split(this string s, int length) { System.Globalization.StringInfo str = new System.Globalization.StringInfo(s); int lengthAbs = Math.Abs(length); if (str == null || str.LengthInTextElements == 0 || lengthAbs == 0 || str.LengthInTextElements <= lengthAbs) return new string[] { str.ToString() }; string[] array = new string[(str.LengthInTextElements % lengthAbs == 0 ? str.LengthInTextElements / lengthAbs: (str.LengthInTextElements / lengthAbs) + 1)]; if (length > 0) for (int iStr = 0, iArray = 0; iStr < str.LengthInTextElements && iArray < array.Length; iStr += lengthAbs, iArray++) array[iArray] = str.SubstringByTextElements(iStr, (str.LengthInTextElements - iStr < lengthAbs ? str.LengthInTextElements - iStr : lengthAbs)); else // if (length < 0) for (int iStr = str.LengthInTextElements - 1, iArray = array.Length - 1; iStr >= 0 && iArray >= 0; iStr -= lengthAbs, iArray--) array[iArray] = str.SubstringByTextElements((iStr - lengthAbs < 0 ? 0 : iStr - lengthAbs + 1), (iStr - lengthAbs < 0 ? iStr + 1 : lengthAbs)); return array; } } } Also, here is an image link to the results of running this code: http://i.imgur.com/16Iih.png
It's not pretty and it's not fast, but it works, it's a one-liner and it's LINQy: List<string> a = text.Select((c, i) => new { Char = c, Index = i }).GroupBy(o => o.Index / 4).Select(g => new String(g.Select(o => o.Char).ToArray())).ToList();
This should be much faster and more efficient than using LINQ or other approaches used here. public static IEnumerable<string> Splice(this string s, int spliceLength) { if (s == null) throw new ArgumentNullException("s"); if (spliceLength < 1) throw new ArgumentOutOfRangeException("spliceLength"); if (s.Length == 0) yield break; var start = 0; for (var end = spliceLength; end < s.Length; end += spliceLength) { yield return s.Substring(start, spliceLength); start = end; } yield return s.Substring(start); }
You can use morelinq by Jon Skeet. Use Batch like: string str = "1111222233334444"; int chunkSize = 4; var chunks = str.Batch(chunkSize).Select(r => new String(r.ToArray())); This will return 4 chunks for the string "1111222233334444". If the string length is less than or equal to the chunk size Batch will return the string as the only element of IEnumerable<string> For output: foreach (var chunk in chunks) { Console.WriteLine(chunk); } and it will give: 1111 2222 3333 4444
Personally I prefer my solution :-) It handles: String lengths that are a multiple of the chunk size. String lengths that are NOT a multiple of the chunk size. String lengths that are smaller than the chunk size. NULL and empty strings (throws an exception). Chunk sizes smaller than 1 (throws an exception). It is implemented as a extension method, and it calculates the number of chunks is going to generate beforehand. It checks the last chunk because in case the text length is not a multiple it needs to be shorter. Clean, short, easy to understand... and works! public static string[] Split(this string value, int chunkSize) { if (string.IsNullOrEmpty(value)) throw new ArgumentException("The string cannot be null."); if (chunkSize < 1) throw new ArgumentException("The chunk size should be equal or greater than one."); int remainder; int divResult = Math.DivRem(value.Length, chunkSize, out remainder); int numberOfChunks = remainder > 0 ? divResult + 1 : divResult; var result = new string[numberOfChunks]; int i = 0; while (i < numberOfChunks - 1) { result[i] = value.Substring(i * chunkSize, chunkSize); i++; } int lastChunkSize = remainder > 0 ? remainder : chunkSize; result[i] = value.Substring(i * chunkSize, lastChunkSize); return result; }
Simple and short: // this means match a space or not a space (anything) up to 4 characters var lines = Regex.Matches(str, #"[\s\S]{0,4}").Cast<Match>().Select(x => x.Value);
I know question is years old, but here is a Rx implementation. It handles the length % chunkSize != 0 problem out of the box: public static IEnumerable<string> Chunkify(this string input, int size) { if(size < 1) throw new ArgumentException("size must be greater than 0"); return input.ToCharArray() .ToObservable() .Buffer(size) .Select(x => new string(x.ToArray())) .ToEnumerable(); }
public static IEnumerable<IEnumerable<T>> SplitEvery<T>(this IEnumerable<T> values, int n) { var ls = values.Take(n); var rs = values.Skip(n); return ls.Any() ? Cons(ls, SplitEvery(rs, n)) : Enumerable.Empty<IEnumerable<T>>(); } public static IEnumerable<T> Cons<T>(T x, IEnumerable<T> xs) { yield return x; foreach (var xi in xs) yield return xi; }
Best , Easiest and Generic Answer :). string originalString = "1111222233334444"; List<string> test = new List<string>(); int chunkSize = 4; // change 4 with the size of strings you want. for (int i = 0; i < originalString.Length; i = i + chunkSize) { if (originalString.Length - i >= chunkSize) test.Add(originalString.Substring(i, chunkSize)); else test.Add(originalString.Substring(i,((originalString.Length - i)))); }
static IEnumerable<string> Split(string str, int chunkSize) { IEnumerable<string> retVal = Enumerable.Range(0, str.Length / chunkSize) .Select(i => str.Substring(i * chunkSize, chunkSize)) if (str.Length % chunkSize > 0) retVal = retVal.Append(str.Substring(str.Length / chunkSize * chunkSize, str.Length % chunkSize)); return retVal; } It correctly handles input string length not divisible by chunkSize. Please note that additional code might be required to gracefully handle edge cases (null or empty input string, chunkSize == 0).
static IEnumerable<string> Split(string str, double chunkSize) { return Enumerable.Range(0, (int) Math.Ceiling(str.Length/chunkSize)) .Select(i => new string(str .Skip(i * (int)chunkSize) .Take((int)chunkSize) .ToArray())); } and another approach: using System; using System.Collections.Generic; using System.Linq; public class Program { public static void Main() { var x = "Hello World"; foreach(var i in x.ChunkString(2)) Console.WriteLine(i); } } public static class Ext{ public static IEnumerable<string> ChunkString(this string val, int chunkSize){ return val.Select((x,i) => new {Index = i, Value = x}) .GroupBy(x => x.Index/chunkSize, x => x.Value) .Select(x => string.Join("",x)); } }
Six years later o_O Just because public static IEnumerable<string> Split(this string str, int chunkSize, bool remainingInFront) { var count = (int) Math.Ceiling(str.Length/(double) chunkSize); Func<int, int> start = index => remainingInFront ? str.Length - (count - index)*chunkSize : index*chunkSize; Func<int, int> end = index => Math.Min(str.Length - Math.Max(start(index), 0), Math.Min(start(index) + chunkSize - Math.Max(start(index), 0), chunkSize)); return Enumerable.Range(0, count).Select(i => str.Substring(Math.Max(start(i), 0),end(i))); } or private static Func<bool, int, int, int, int, int> start = (remainingInFront, length, count, index, size) => remainingInFront ? length - (count - index) * size : index * size; private static Func<bool, int, int, int, int, int, int> end = (remainingInFront, length, count, index, size, start) => Math.Min(length - Math.Max(start, 0), Math.Min(start + size - Math.Max(start, 0), size)); public static IEnumerable<string> Split(this string str, int chunkSize, bool remainingInFront) { var count = (int)Math.Ceiling(str.Length / (double)chunkSize); return Enumerable.Range(0, count).Select(i => str.Substring( Math.Max(start(remainingInFront, str.Length, count, i, chunkSize), 0), end(remainingInFront, str.Length, count, i, chunkSize, start(remainingInFront, str.Length, count, i, chunkSize)) )); } AFAIK all edge cases are handled. Console.WriteLine(string.Join(" ", "abc".Split(2, false))); // ab c Console.WriteLine(string.Join(" ", "abc".Split(2, true))); // a bc Console.WriteLine(string.Join(" ", "a".Split(2, true))); // a Console.WriteLine(string.Join(" ", "a".Split(2, false))); // a
List<string> SplitString(int chunk, string input) { List<string> list = new List<string>(); int cycles = input.Length / chunk; if (input.Length % chunk != 0) cycles++; for (int i = 0; i < cycles; i++) { try { list.Add(input.Substring(i * chunk, chunk)); } catch { list.Add(input.Substring(i * chunk)); } } return list; }
I took this to another level. Chucking is an easy one liner, but in my case I needed whole words as well. Figured I would post it, just in case someone else needs something similar. static IEnumerable<string> Split(string orgString, int chunkSize, bool wholeWords = true) { if (wholeWords) { List<string> result = new List<string>(); StringBuilder sb = new StringBuilder(); if (orgString.Length > chunkSize) { string[] newSplit = orgString.Split(' '); foreach (string str in newSplit) { if (sb.Length != 0) sb.Append(" "); if (sb.Length + str.Length > chunkSize) { result.Add(sb.ToString()); sb.Clear(); } sb.Append(str); } result.Add(sb.ToString()); } else result.Add(orgString); return result; } else return new List<string>(Regex.Split(orgString, #"(?<=\G.{" + chunkSize + "})", RegexOptions.Singleline)); } Results based on below comment: string msg = "336699AABBCCDDEEFF"; foreach (string newMsg in Split(msg, 2, false)) { Console.WriteLine($">>{newMsg}<<"); } Console.ReadKey(); Results: >>33<< >>66<< >>99<< >>AA<< >>BB<< >>CC<< >>DD<< >>EE<< >>FF<< >><< Another way to pull it: List<string> splitData = (List<string>)Split(msg, 2, false); for (int i = 0; i < splitData.Count - 1; i++) { Console.WriteLine($">>{splitData[i]}<<"); } Console.ReadKey(); New Results: >>33<< >>66<< >>99<< >>AA<< >>BB<< >>CC<< >>DD<< >>EE<< >>FF<<
An important tip if the string that is being chunked needs to support all Unicode characters. If the string is to support international characters like 𠀋, then split up the string using the System.Globalization.StringInfo class. Using StringInfo, you can split up the string based on number of text elements. string internationalString = '𠀋'; The above string has a Length of 2, because the String.Length property returns the number of Char objects in this instance, not the number of Unicode characters.
Changed slightly to return parts whose size not equal to chunkSize public static IEnumerable<string> Split(this string str, int chunkSize) { var splits = new List<string>(); if (str.Length < chunkSize) { chunkSize = str.Length; } splits.AddRange(Enumerable.Range(0, str.Length / chunkSize).Select(i => str.Substring(i * chunkSize, chunkSize))); splits.Add(str.Length % chunkSize > 0 ? str.Substring((str.Length / chunkSize) * chunkSize, str.Length - ((str.Length / chunkSize) * chunkSize)) : string.Empty); return (IEnumerable<string>)splits; }
I think this is an straight forward answer: public static IEnumerable<string> Split(this string str, int chunkSize) { if(string.IsNullOrEmpty(str) || chunkSize<1) throw new ArgumentException("String can not be null or empty and chunk size should be greater than zero."); var chunkCount = str.Length / chunkSize + (str.Length % chunkSize != 0 ? 1 : 0); for (var i = 0; i < chunkCount; i++) { var startIndex = i * chunkSize; if (startIndex + chunkSize >= str.Length) yield return str.Substring(startIndex); else yield return str.Substring(startIndex, chunkSize); } } And it covers edge cases.
static List<string> GetChunks(string value, int chunkLength) { var res = new List<string>(); int count = (value.Length / chunkLength) + (value.Length % chunkLength > 0 ? 1 : 0); Enumerable.Range(0, count).ToList().ForEach(f => res.Add(value.Skip(f * chunkLength).Take(chunkLength).Select(z => z.ToString()).Aggregate((a,b) => a+b))); return res; } demo
Here's my 2 cents: IEnumerable<string> Split(string str, int chunkSize) { while (!string.IsNullOrWhiteSpace(str)) { var chunk = str.Take(chunkSize).ToArray(); str = str.Substring(chunk.Length); yield return new string(chunk); } }//Split
I've slightly build up on João's solution. What I've done differently is in my method you can actually specify whether you want to return the array with remaining characters or whether you want to truncate them if the end characters do not match your required chunk length, I think it's pretty flexible and the code is fairly straight forward: using System; using System.Linq; using System.Text.RegularExpressions; namespace SplitFunction { class Program { static void Main(string[] args) { string text = "hello, how are you doing today?"; string[] chunks = SplitIntoChunks(text, 3,false); if (chunks != null) { chunks.ToList().ForEach(e => Console.WriteLine(e)); } Console.ReadKey(); } private static string[] SplitIntoChunks(string text, int chunkSize, bool truncateRemaining) { string chunk = chunkSize.ToString(); string pattern = truncateRemaining ? ".{" + chunk + "}" : ".{1," + chunk + "}"; string[] chunks = null; if (chunkSize > 0 && !String.IsNullOrEmpty(text)) chunks = (from Match m in Regex.Matches(text,pattern)select m.Value).ToArray(); return chunks; } } }
public static List<string> SplitByMaxLength(this string str) { List<string> splitString = new List<string>(); for (int index = 0; index < str.Length; index += MaxLength) { splitString.Add(str.Substring(index, Math.Min(MaxLength, str.Length - index))); } return splitString; }
I can't remember who gave me this, but it works great. I speed tested a number of ways to break Enumerable types into groups. The usage would just be like this... List<string> Divided = Source3.Chunk(24).Select(Piece => string.Concat<char>(Piece)).ToList(); The extention code would look like this... #region Chunk Logic private class ChunkedEnumerable<T> : IEnumerable<T> { class ChildEnumerator : IEnumerator<T> { ChunkedEnumerable<T> parent; int position; bool done = false; T current; public ChildEnumerator(ChunkedEnumerable<T> parent) { this.parent = parent; position = -1; parent.wrapper.AddRef(); } public T Current { get { if (position == -1 || done) { throw new InvalidOperationException(); } return current; } } public void Dispose() { if (!done) { done = true; parent.wrapper.RemoveRef(); } } object System.Collections.IEnumerator.Current { get { return Current; } } public bool MoveNext() { position++; if (position + 1 > parent.chunkSize) { done = true; } if (!done) { done = !parent.wrapper.Get(position + parent.start, out current); } return !done; } public void Reset() { // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx throw new NotSupportedException(); } } EnumeratorWrapper<T> wrapper; int chunkSize; int start; public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start) { this.wrapper = wrapper; this.chunkSize = chunkSize; this.start = start; } public IEnumerator<T> GetEnumerator() { return new ChildEnumerator(this); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } private class EnumeratorWrapper<T> { public EnumeratorWrapper(IEnumerable<T> source) { SourceEumerable = source; } IEnumerable<T> SourceEumerable { get; set; } Enumeration currentEnumeration; class Enumeration { public IEnumerator<T> Source { get; set; } public int Position { get; set; } public bool AtEnd { get; set; } } public bool Get(int pos, out T item) { if (currentEnumeration != null && currentEnumeration.Position > pos) { currentEnumeration.Source.Dispose(); currentEnumeration = null; } if (currentEnumeration == null) { currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false }; } item = default(T); if (currentEnumeration.AtEnd) { return false; } while (currentEnumeration.Position < pos) { currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext(); currentEnumeration.Position++; if (currentEnumeration.AtEnd) { return false; } } item = currentEnumeration.Source.Current; return true; } int refs = 0; // needed for dispose semantics public void AddRef() { refs++; } public void RemoveRef() { refs--; if (refs == 0 && currentEnumeration != null) { var copy = currentEnumeration; currentEnumeration = null; copy.Source.Dispose(); } } } /// <summary>Speed Checked. Works Great!</summary> public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize) { if (chunksize < 1) throw new InvalidOperationException(); var wrapper = new EnumeratorWrapper<T>(source); int currentPos = 0; T ignore; try { wrapper.AddRef(); while (wrapper.Get(currentPos, out ignore)) { yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos); currentPos += chunksize; } } finally { wrapper.RemoveRef(); } } #endregion
class StringHelper { static void Main(string[] args) { string str = "Hi my name is vikas bansal and my email id is bansal.vks#gmail.com"; int offSet = 10; List<string> chunks = chunkMyStr(str, offSet); Console.Read(); } static List<string> chunkMyStr(string str, int offSet) { List<string> resultChunks = new List<string>(); for (int i = 0; i < str.Length; i += offSet) { string temp = str.Substring(i, (str.Length - i) > offSet ? offSet : (str.Length - i)); Console.WriteLine(temp); resultChunks.Add(temp); } return resultChunks; } }
Split a range sequence into multiple string c#,linq [closed]
Closed. This question needs debugging details. It is not currently accepting answers. Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question. Closed 7 years ago. Improve this question Not sure why question is being marked as offtopic, where as so called desired behaviour is included within the question post! I am trying to write this program that takes two inputs: • a set of include intervals • and a set of exclude intervals The sets of intervals can be given in any order, and they may be empty or overlapping. The program should output the result of taking all the includes and “remove” the excludes. The output should be given as non-overlapping intervals in a sorted order. Intervals will contain Integers only Example : Includes: 50-600, 10-100 Excludes: (empty) Output: 10-600 Includes: 10-100, 200-300, 400-600 Excludes: 95-205, 410-420 Output: 10-94, 206-300, 400-409, 421-600 I tried to populate two Enumerable Range from include and excludes (after splitting,parsing ), but didn't find any efficient way of implementing this afterwards. string[] _break = _string.Split(','); string[] _breakB = _stringB.Split(','); string[] res = new string[_break.Length + 1]; string[] _items, _itemsB; List < int > _back = new List < int > (); int count = 0; foreach(var _item in _break) { _items = _item.Split('-'); var a = Enumerable.Range(int.Parse(_items[0]), (int.Parse(_items[1]) - int.Parse(_items[0]) + 1)).ToList(); foreach(var _itemB in _breakB) { _itemsB = _itemB.Split('-'); var b = Enumerable.Range(int.Parse((_itemsB[0])), (int.Parse(_itemsB[1]) - int.Parse((_itemsB[0])) + 1)).ToList(); var c = a.Except < int > (b).ToList(); /// different things tried here, but they are not good res[count] = c.Min().ToString() + "-" + c.Max().ToString(); count++; } } return res; Any input will be of great help
You can use the Built-in SortedSet<T> collection to do most of the work for you like this: The SortedSet<T> collection implements the useful UnionWith and ExceptWith methods which at least makes the code quite easy to follow: private void button1_Click(object sender, EventArgs e) { string[] includeRanges = _string.Text.Replace(" ", "").Split(','); string[] excludeRanges = _stringB.Text.Replace(" ", "").Split(','); string[] includeRange, excludeRange; SortedSet<int> includeSet = new SortedSet<int>(); SortedSet<int> excludeSet = new SortedSet<int>(); // Create a UNION of all the include ranges foreach (string item in includeRanges) { includeRange = item.Split('-'); includeSet.UnionWith(Enumerable.Range(int.Parse(includeRange[0]), (int.Parse(includeRange[1]) - int.Parse(includeRange[0]) + 1)).ToList()); } // Create a UNION of all the exclude ranges foreach (string item in excludeRanges) { excludeRange = item.Split('-'); excludeSet.UnionWith(Enumerable.Range(int.Parse(excludeRange[0]), (int.Parse(excludeRange[1]) - int.Parse(excludeRange[0]) + 1)).ToList()); } // Exclude the excludeSet from the includeSet includeSet.ExceptWith(excludeSet); //Format the output using a stringbuilder StringBuilder sb = new StringBuilder(); int lastValue = -1; foreach (int included in includeSet) { if (lastValue == -1) { sb.Append(included + "-"); lastValue = included; } else { if (lastValue == included - 1) { lastValue = included; } else { sb.Append(lastValue + ","); sb.Append(included + "-"); lastValue = included; } } } sb.Append(lastValue); result.Text = sb.ToString(); }
This should work faster than SortedSet trick, at least for large intervals. Idea is like: using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace Test { using Pair = Tuple<int, int>; //for brevity struct Point //point of an interval { public enum Border { Left, Right }; public enum Interval { Including, Excluding }; public int Val; public int Brdr; public int Intr; public Point(int value, Border border, Interval interval) { Val = value; Brdr = (border == Border.Left) ? 1 : -1; Intr = (int)interval; } public override string ToString() => (Brdr == 1 ? "L" : "R") + (Intr == 0 ? "+ " : "- ") + Val; } class Program { static IEnumerable<Pair> GetInterval(string strIn, string strEx) { //a func to get interval border points from string: Func<string, Point.Interval, IEnumerable<Point>> parse = (str, intr) => Regex.Matches(str, "[0-9]+").Cast<Match>().Select((s, idx) => new Point(int.Parse(s.Value), (Point.Border)(idx % 2), intr)); var INs = parse(strIn, Point.Interval.Including); var EXs = parse(strEx, Point.Interval.Excluding); var intrs = new int[2]; //current interval border control IN[0], EX[1] int start = 0; //left border of a new resulting interval //put all points in a line and loop: foreach (var p in INs.Union(EXs).OrderBy(x => x.Val)) { //check for start (close) of a new (cur) interval: var change = (intrs[p.Intr] == 0) ^ (intrs[p.Intr] + p.Brdr == 0); intrs[p.Intr] += p.Brdr; if (!change) continue; var In = p.Intr == 0 && intrs[1] == 0; //w no Ex var Ex = p.Intr == 1 && intrs[0] > 0; //breaks In var Open = intrs[p.Intr] > 0; var Close = !Open; if (In && Open || Ex && Close) { start = p.Val + p.Intr; //exclude point if Ex } else if (In && Close || Ex && Open) { yield return new Pair(start, p.Val - p.Intr); } } } static void Main(string[] args) { var strIN = "10-100, 200-300, 400-500, 420-480"; var strEX = "95-205, 410-420"; foreach (var i in GetInterval(strIN, strEX)) Console.WriteLine(i.Item1 + "-" + i.Item2); Console.ReadLine(); } } }
So, you task could be separated to the list of subtasks: Parse a source line of intervals to the list of objects Concatinate intervals if they cross each over Excludes intervals 'excludes' from 'includes' I published my result code here: http://rextester.com/OBXQ56769 The code could be optimized as well, but I wanted it to be quite simple. Hope it will help you. using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace ConsoleApplication { public class Program { private const string Includes = "10-100, 200-300, 400-500 "; private const string Excludes = "95-205, 410-420"; private const string Pattern = #"(\d*)-(\d*)"; public static void Main(string[] args) { var includes = ParseIntevals(Includes); var excludes = ParseIntevals(Excludes); includes = ConcatinateIntervals(includes); excludes = ConcatinateIntervals(excludes); // The Result var result = ExcludeFromInclude(includes, excludes); foreach (var interval in result) { Console.WriteLine(interval.Min + "-" + interval.Max); } } /// <summary> /// Excludes intervals 'excludes' from 'includes' /// </summary> public static List<Interval> ExcludeFromInclude(List<Interval> includes, List<Interval> excludes) { var result = new List<Interval>(); if (!excludes.Any()) { return includes.Select(x => x.Clone()).ToList(); } for (int i = 0; i < includes.Count; i++) { for (int j = 0; j < excludes.Count; j++) { if (includes[i].Max < excludes[j].Min || includes[i].Min > excludes[j].Max) continue; // no crossing //1 Example: includes[i]=(10-20) excludes[j]=(15-25) if (includes[i].Min < excludes[j].Min && includes[i].Max <= excludes[j].Max) { var interval = new Interval(includes[i].Min, excludes[j].Min - 1); result.Add(interval); break; } //2 Example: includes[i]=(10-25) excludes[j]=(15-20) if (includes[i].Min <= excludes[j].Min && includes[i].Max >= excludes[j].Max) { if (includes[i].Min < excludes[j].Min) { var interval1 = new Interval(includes[i].Min, excludes[j].Min - 1); result.Add(interval1); } if (includes[i].Max > excludes[j].Max) { var interval2 = new Interval(excludes[j].Max + 1, includes[i].Max); result.Add(interval2); } break; } //3 Example: includes[i]=(15-25) excludes[j]=(10-20) if (includes[i].Min < excludes[j].Max && includes[i].Max > excludes[j].Max) { var interval = new Interval(excludes[j].Max + 1, includes[i].Max); result.Add(interval); break; } } } return result; } /// <summary> /// Concatinates intervals if they cross each over /// </summary> public static List<Interval> ConcatinateIntervals(List<Interval> intervals) { var result = new List<Interval>(); for (int i = 0; i < intervals.Count; i++) { for (int j = 0; j < intervals.Count; j++) { if (i == j) continue; if (intervals[i].Max < intervals[j].Min || intervals[i].Min > intervals[j].Max) { Interval interval = intervals[i].Clone(); result.Add(interval); continue; // no crossing } //1 if (intervals[i].Min < intervals[j].Min && intervals[i].Max < intervals[j].Max) { var interval = new Interval(intervals[i].Min, intervals[j].Max); result.Add(interval); break; } //2 if (intervals[i].Min < intervals[j].Min && intervals[i].Max > intervals[j].Max) { Interval interval = intervals[i].Clone(); result.Add(interval); break; } //3 if (intervals[i].Min < intervals[j].Max && intervals[i].Max > intervals[j].Max) { var interval = new Interval(intervals[j].Min, intervals[i].Max); result.Add(interval); break; } //4 if (intervals[i].Min > intervals[j].Min && intervals[i].Max < intervals[j].Max) { var interval = new Interval(intervals[j].Min, intervals[j].Max); result.Add(interval); break; } } } return result.Distinct().ToList(); } /// <summary> /// Parses a source line of intervals to the list of objects /// </summary> public static List<Interval> ParseIntevals(string intervals) { var matches = Regex.Matches(intervals, Pattern, RegexOptions.IgnoreCase); var list = new List<Interval>(); foreach (Match match in matches) { var min = int.Parse(match.Groups[1].Value); var max = int.Parse(match.Groups[2].Value); list.Add(new Interval(min, max)); } return list.OrderBy(x => x.Min).ToList(); } /// <summary> /// Interval /// </summary> public class Interval { public int Min { get; set; } public int Max { get; set; } public Interval() { } public Interval(int min, int max) { Min = min; Max = max; } public override bool Equals(object obj) { var obj2 = obj as Interval; if (obj2 == null) return false; return obj2.Min == Min && obj2.Max == Max; } public override int GetHashCode() { return this.ToString().GetHashCode(); } public override string ToString() { return string.Format("{0}-{1}", Min, Max); } public Interval Clone() { return (Interval) this.MemberwiseClone(); } } } }
Lots of ways to solve this. The LINQ approach hasn't been discussed yet - this is how I would do it: // declaring a lambda fn because it's gonna be used by both include/exclude // list Func<string, IEnumerable<int>> rangeFn = baseInput => { return baseInput.Split (new []{ ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) .SelectMany (rng => { var range = rng.Split (new []{ '-' }, StringSplitOptions.RemoveEmptyEntries) .Select(i => Convert.ToInt32(i)); // just in case someone types in // a reverse range (e.g. 10-5), LOL... var start = range.Min (); var end = range.Max (); return Enumerable.Range (start, (end - start + 1)); }); }; var includes = rangeFn (_string); var excludes = rangeFn (_stringB); var result = includes.Except (excludes).Distinct().OrderBy(r => r);
Is there a higher performance method for removing rare unwanted chars from a string?
EDIT Apologies if the original unedited question is misleading. This question is not asking how to remove Invalid XML Chars from a string, answers to that question would be better directed here. I'm not asking you to review my code. What I'm looking for in answers is, a function with the signature string <YourName>(string input, Func<char, bool> check); that will have performance similar or better than RemoveCharsBufferCopyBlackList. Ideally this function would be more generic and if possible simpler to read, but these requirements are secondary. I recently wrote a function to strip invalid XML chars from a string. In my application the strings can be modestly long and the invalid chars occur rarely. This excerise got me thinking. What ways can this be done in safe managed c# and, which would offer the best performance for my scenario. Here is my test program, I've subtituted the "valid XML predicate" for one the omits the char 'X'. class Program { static void Main() { var attempts = new List<Func<string, Func<char, bool>, string>> { RemoveCharsLinqWhiteList, RemoveCharsFindAllWhiteList, RemoveCharsBufferCopyBlackList } const string GoodString = "1234567890abcdefgabcedefg"; const string BadString = "1234567890abcdefgXabcedefg"; const int Iterations = 100000; var timer = new StopWatch(); var testSet = new List<string>(Iterations); for (var i = 0; i < Iterations; i++) { if (i % 1000 == 0) { testSet.Add(BadString); } else { testSet.Add(GoodString); } } foreach (var attempt in attempts) { //Check function works and JIT if (attempt.Invoke(BadString, IsNotUpperX) != GoodString) { throw new ApplicationException("Broken Function"); } if (attempt.Invoke(GoodString, IsNotUpperX) != GoodString) { throw new ApplicationException("Broken Function"); } timer.Reset(); timer.Start(); foreach (var t in testSet) { attempt.Invoke(t, IsNotUpperX); } timer.Stop(); Console.WriteLine( "{0} iterations of function \"{1}\" performed in {2}ms", Iterations, attempt.Method, timer.ElapsedMilliseconds); Console.WriteLine(); } Console.Readkey(); } private static bool IsNotUpperX(char value) { return value != 'X'; } private static string RemoveCharsLinqWhiteList(string input, Func<char, bool> check); { return new string(input.Where(check).ToArray()); } private static string RemoveCharsFindAllWhiteList(string input, Func<char, bool> check); { return new string(Array.FindAll(input.ToCharArray(), check.Invoke)); } private static string RemoveCharsBufferCopyBlackList(string input, Func<char, bool> check); { char[] inputArray = null; char[] outputBuffer = null; var blackCount = 0; var lastb = -1; var whitePos = 0; for (var b = 0; b , input.Length; b++) { if (!check.invoke(input[b])) { var whites = b - lastb - 1; if (whites > 0) { if (outputBuffer == null) { outputBuffer = new char[input.Length - blackCount]; } if (inputArray == null) { inputArray = input.ToCharArray(); } Buffer.BlockCopy( inputArray, (lastb + 1) * 2, outputBuffer, whitePos * 2, whites * 2); whitePos += whites; } lastb = b; blackCount++; } } if (blackCount == 0) { return input; } var remaining = inputArray.Length - 1 - lastb; if (remaining > 0) { Buffer.BlockCopy( inputArray, (lastb + 1) * 2, outputBuffer, whitePos * 2, remaining * 2); } return new string(outputBuffer, 0, inputArray.Length - blackCount); } } If you run the attempts you'll note that the performance improves as the functions get more specialised. Is there a faster and more generic way to perform this operation? Or if there is no generic option is there a way that is just faster? Please note that I am not actually interested in removing 'X' and in practice the predicate is more complicated.
You certainly don't want to use LINQ to Objects aka enumerators to do this if you require high performance. Also, don't invoke a delegate per char. Delegate invocations are costly compared to the actual operation you are doing. RemoveCharsBufferCopyBlackList looks good (except for the delegate call per character). I recommend that you inline the contents of the delegate hard-coded. Play around with different ways to write the condition. You may get better performance by first checking the current char against a range of known good chars (e.g. 0x20-0xFF) and if it matches let it through. This test will pass almost always so you can save the expensive checks against individual characters which are invalid in XML. Edit: I just remembered I solved this problem a while ago: static readonly string invalidXmlChars = Enumerable.Range(0, 0x20) .Where(i => !(i == '\u000A' || i == '\u000D' || i == '\u0009')) .Select(i => (char)i) .ConcatToString() + "\uFFFE\uFFFF"; public static string RemoveInvalidXmlChars(string str) { return RemoveInvalidXmlChars(str, false); } internal static string RemoveInvalidXmlChars(string str, bool forceRemoveSurrogates) { if (str == null) throw new ArgumentNullException("str"); if (!ContainsInvalidXmlChars(str, forceRemoveSurrogates)) return str; str = str.RemoveCharset(invalidXmlChars); if (forceRemoveSurrogates) { for (int i = 0; i < str.Length; i++) { if (IsSurrogate(str[i])) { str = str.Where(c => !IsSurrogate(c)).ConcatToString(); break; } } } return str; } static bool IsSurrogate(char c) { return c >= 0xD800 && c < 0xE000; } internal static bool ContainsInvalidXmlChars(string str) { return ContainsInvalidXmlChars(str, false); } public static bool ContainsInvalidXmlChars(string str, bool forceRemoveSurrogates) { if (str == null) throw new ArgumentNullException("str"); for (int i = 0; i < str.Length; i++) { if (str[i] < 0x20 && !(str[i] == '\u000A' || str[i] == '\u000D' || str[i] == '\u0009')) return true; if (str[i] >= 0xD800) { if (forceRemoveSurrogates && str[i] < 0xE000) return true; if ((str[i] == '\uFFFE' || str[i] == '\uFFFF')) return true; } } return false; } Notice, that RemoveInvalidXmlChars first invokes ContainsInvalidXmlChars to save the string allocation. Most strings do not contain invalid XML chars so we can be optimistic.
How to sort list of Ip Addresses using c#
I've a list of IP addresses as follows 192.168.1.5 69.52.220.44 10.152.16.23 192.168.3.10 192.168.1.4 192.168.2.1 I'm looking for such a way to sort this list to match the below order 10.152.16.23 69.52.220.44 192.168.1.4 192.168.1.5 192.168.2.1
This might look as a hack, but it does exactly what you need: var unsortedIps = new[] { "192.168.1.4", "192.168.1.5", "192.168.2.1", "10.152.16.23", "69.52.220.44" }; var sortedIps = unsortedIps .Select(Version.Parse) .OrderBy(arg => arg) .Select(arg => arg.ToString()) .ToList();
You can convert each IP address into an integer like so ... 69.52.220.44 => 69 * 255 * 255 * 255 + 52 * 255 * 255 + 220 * 255 + 44 Then sort by the integer representation.
You may find this function useful too. public static class ExtensionMethods { public static int CompareTo(this IPAddress x, IPAddress y) { var result = x.AddressFamily.CompareTo(y.AddressFamily); if (result != 0) return result; var xBytes = x.GetAddressBytes(); var yBytes = y.GetAddressBytes(); var octets = Math.Min(xBytes.Length, yBytes.Length); for (var i = 0; i < octets; i++) { var octetResult = xBytes[i].CompareTo(yBytes[i]); if (octetResult != 0) return octetResult; } return 0; } }
No hackery required for a simple solution. Just Split() the address segments, pad the segments with zeros, and then join them back together. Put this one line static method into a static class like this: public static class StringHelper { public static string IpAddressLabel(string ipAddress) => string.Join(".", ipAddress.Split('.').Select(part => part.PadLeft(3, '0'))); } And then call it at will: => new[] {"192.168.1.100", "192.168.1.1", "192.168.1.19"} .OrderBy(ip => StringHelper.IpAddressLabel(ip)); Also, this can be used as a filename or elsewhere when a sortable label is desired: 192.168.001.001.log 192.168.001.019.log 192.168.001.100.log
You can use the Array.Sort function with a function we will create for comparing two IPs: //ips is string array Array.Sort(ips, IpCompare); And then put this function in the code. private static int IpCompare(string x, string y) { string ip1 = x + '.', ip2 = y + '.'; string xSection = "", ySection = ""; for (int i = 0; i < ip1.Length && i < ip2.Length; i++) { if (ip1[i] == '.' && ip2[i] == '.') { if (xSection != ySection) return int.Parse(xSection) - int.Parse(ySection); xSection = ""; // Start compare the next section ySection = ""; } else if (ip1[i] == '.') return -1; //The first section is smaller because it's length is smaller else if (ip2[i] == '.') return 1; else { xSection += ip1[i]; ySection += ip2[i]; } } return 0; //If we would find any difference between any section it would already return something. //so that mean that both IPs are the same }
IComparer for integers and force empty strings to end
I've written the following IComparer but I need some help. I'm trying to sort a list of numbers but some of the numbers may not have been filled in. I want these numbers to be sent to the end of the list at all times.. for example... [EMPTY], 1, [EMPTY], 3, 2 would become... 1, 2, 3, [EMPTY], [EMPTY] and reversed this would become... 3, 2, 1, [EMPTY], [EMPTY] Any ideas? public int Compare(ListViewItem x, ListViewItem y) { int comparison = int.MinValue; ListViewItem.ListViewSubItem itemOne = x.SubItems[subItemIndex]; ListViewItem.ListViewSubItem itemTwo = y.SubItems[subItemIndex]; if (!string.IsNullOrEmpty(itemOne.Text) && !string.IsNullOrEmpty(itemTwo.Text)) { uint itemOneComparison = uint.Parse(itemOne.Text); uint itemTwoComparison = uint.Parse(itemTwo.Text); comparison = itemOneComparison.CompareTo(itemTwoComparison); } else { // ALWAYS SEND TO BOTTOM/END OF LIST. } // Calculate correct return value based on object comparison. if (OrderOfSort == SortOrder.Descending) { // Descending sort is selected, return negative result of compare operation. comparison = (-comparison); } else if (OrderOfSort == SortOrder.None) { // Return '0' to indicate they are equal. comparison = 0; } return comparison; } Cheers.
Your logic is slightly off: your else will be entered if either of them are empty, but you only want the empty one to go to the end of the list, not the non-empty one. Something like this should work: public int Compare(ListViewItem x, ListViewItem y) { ListViewItem.ListViewSubItem itemOne = x.SubItems[subItemIndex]; ListViewItem.ListViewSubItem itemTwo = y.SubItems[subItemIndex]; // if they're both empty, return 0 if (string.IsNullOrEmpty(itemOne.Text) && string.IsNullOrEmpty(itemTwo.Text)) return 0; // if itemOne is empty, it comes second if (string.IsNullOrEmpty(itemOne.Text)) return 1; // if itemTwo is empty, it comes second if (string.IsNullOrEmpty(itemTwo.Text) return -1; uint itemOneComparison = uint.Parse(itemOne.Text); uint itemTwoComparison = uint.Parse(itemTwo.Text); // Calculate correct return value based on object comparison. int comparison = itemOneComparison.CompareTo(itemTwoComparison); if (OrderOfSort == SortOrder.Descending) comparison = (-comparison); return comparison; } (I might've got the "1" and "-1" for when they're empty back to front, I can never remember :)
I'd actually approach this a completely different way, remove the empty slots, sort the list, then add the empty ones to the end of the list static void Main(string[] args) { List<string> ints = new List<string> { "3", "1", "", "5", "", "2" }; CustomIntSort(ints, (x, y) => int.Parse(x) - int.Parse(y)); // Ascending ints.ForEach(i => Console.WriteLine("[{0}]", i)); CustomIntSort(ints, (x, y) => int.Parse(y) - int.Parse(x)); // Descending ints.ForEach(i => Console.WriteLine("[{0}]", i)); } private static void CustomIntSort(List<string> ints, Comparison<string> Comparer) { int emptySlots = CountAndRemove(ints); ints.Sort(Comparer); for (int i = 0; i < emptySlots; i++) ints.Add(""); } private static int CountAndRemove(List<string> ints) { int emptySlots = 0; int i = 0; while (i < ints.Count) { if (string.IsNullOrEmpty(ints[i])) { emptySlots++; ints.RemoveAt(i); } else i++; } return emptySlots; } This question caught my attention recently, this comparer will do it either class CustomComparer : IComparer<string> { private bool isAscending; public CustomComparer(bool isAscending = true) { this.isAscending = isAscending; } public int Compare(string x, string y) { long ix = CustomParser(x) * (isAscending ? 1 : -1); long iy = CustomParser(y) * (isAscending ? 1 : -1); return ix.CompareTo(iy) ; } private long CustomParser(string s) { if (string.IsNullOrEmpty(s)) return isAscending ? int.MaxValue : int.MinValue; else return int.Parse(s); } }
Your // ALWAYS SEND TO BOTTOM/END OF LIST. branch is being executed when either the x or y parameters are empty, i.e. a non-empty value will be sorted according to this rule if it is being compared to an empty value. You probably want something more like this: if (!string.IsNullOrEmpty(itemOne.Text) && !string.IsNullOrEmpty(itemTwo.Text)) { uint itemOneComparison = uint.Parse(itemOne.Text); uint itemTwoComparison = uint.Parse(itemTwo.Text); comparison = itemOneComparison.CompareTo(itemTwoComparison); } else if (!string.IsNullOrEmpty(itemOne.Text) { comparison = -1; } else { comparison = 1; }
Always return 1 for your empty x values and -1 for your empty y values. This will mean that the comparer sees empty values as the greater value in all cases so they should end up at the end of the sorted list. Of course, if both are empty, you should return 0 as they are equal.
else { //ALWAYS SEND TO BOTTOM/END OF LIST. if (string.IsNullOrEmpty(itemOne.Text) && string.IsNullOrEmpty(itemTwo.Text)) { return 0; } else if (string.IsNullOrEmpty(itemOne.Text)) { return -1; } else if (string.IsNullOrEmpty(itemTwo.Text)) { return 1; } }