ใครมีแหล่งข้อมูลที่ดีหรือให้ตัวอย่างการเรียงลำดับธรรมชาติใน C # สำหรับFileInfo
อาร์เรย์ ฉันกำลังใช้งานIComparer
อินเทอร์เฟซในประเภทของฉัน
ใครมีแหล่งข้อมูลที่ดีหรือให้ตัวอย่างการเรียงลำดับธรรมชาติใน C # สำหรับFileInfo
อาร์เรย์ ฉันกำลังใช้งานIComparer
อินเทอร์เฟซในประเภทของฉัน
คำตอบ:
สิ่งที่ง่ายที่สุดที่จะทำคือเพียง P / เรียกใช้ฟังก์ชันในตัวใน Windows และใช้เป็นฟังก์ชันเปรียบเทียบในIComparer
:
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
private static extern int StrCmpLogicalW(string psz1, string psz2);
Michael Kaplan มีตัวอย่างบางส่วนเกี่ยวกับการทำงานของฟังก์ชันนี้ที่นี่และการเปลี่ยนแปลงที่เกิดขึ้นกับ Vista เพื่อให้ใช้งานได้ง่ายขึ้น ข้อดีของฟังก์ชั่นนี้คือมันจะมีลักษณะการทำงานเหมือนกับ Windows เวอร์ชันที่ทำงานอยู่ แต่หมายความว่ามันแตกต่างกันระหว่างเวอร์ชันของ Windows ดังนั้นคุณต้องพิจารณาว่านี่เป็นปัญหาสำหรับคุณหรือไม่
ดังนั้นการใช้งานที่สมบูรณ์จะเป็นดังนี้:
[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);
}
}
Comparer<T>
แทนที่จะใช้งานIComparer<T>
คุณจะได้รับIComparer
อินเทอร์เฟซ (ไม่ใช่ทั่วไป) ในตัวที่เรียกใช้วิธีการทั่วไปของคุณเพื่อใช้ใน API ที่ใช้แทน มันเป็นพื้นอิสระที่จะทำเกินไป: เพียงแค่ลบ "ฉัน" และการเปลี่ยนแปลงไปpublic int Compare(...)
public override int Compare(...)
เหมือนกันสำหรับIEqualityComparer<T>
และEqualityComparer<T>
.
แค่คิดว่าฉันจะเพิ่มสิ่งนี้ (ด้วยวิธีแก้ปัญหาที่รัดกุมที่สุดที่ฉันหาได้):
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')));
}
ด้านบนจะกำหนดตัวเลขใด ๆ ในสตริงให้มีความยาวสูงสุดของตัวเลขทั้งหมดในสตริงทั้งหมดและใช้สตริงผลลัพธ์ในการจัดเรียง
โยนไปที่ ( int?
) คือการอนุญาตให้สำหรับคอลเลกชันของสตริงโดยไม่ต้องตัวเลขใด ๆ ( .Max()
ในที่ว่างเปล่านับพ่นInvalidOperationException
)
.DefaultIfEmpty().Max()
int?
นอกจากนี้ยังควรทำsource.ToList()
เพื่อหลีกเลี่ยงการแจกแจงซ้ำอีกครั้ง
ไม่มีการนำไปใช้งานใดที่ดูดีมากดังนั้นฉันจึงเขียนของตัวเอง ผลลัพธ์เกือบจะเหมือนกับการเรียงลำดับที่ใช้โดย Windows Explorer รุ่นใหม่ (Windows 7/8) ความแตกต่างเพียงอย่างเดียวที่ฉันเห็นคือ 1) แม้ว่า Windows จะใช้ (เช่น XP) จัดการกับตัวเลขที่มีความยาวเท่าใดก็ได้ แต่ตอนนี้ จำกัด ไว้ที่ 19 หลัก - ของฉันไม่ จำกัด 2) Windows ให้ผลลัพธ์ที่ไม่สอดคล้องกันกับชุดตัวเลข Unicode บางชุด - งานของฉัน ดี (แม้ว่าจะไม่ได้เปรียบเทียบตัวเลขจากคู่ตัวแทนหรือ Windows) และ 3) ของฉันไม่สามารถแยกแยะน้ำหนักการเรียงลำดับที่ไม่ใช่หลักประเภทต่างๆได้หากเกิดขึ้นในส่วนต่างๆ (เช่น "e-1é" vs " é1e- "- ส่วนก่อนและหลังตัวเลขมีความแตกต่างของน้ำหนักตัวกำกับเสียงและเครื่องหมายวรรคตอน)
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;
}
ลายเซ็นตรงกับComparison<string>
ผู้รับมอบสิทธิ์:
string[] files = Directory.GetFiles(@"C:\");
Array.Sort(files, CompareNatural);
นี่คือคลาส Wrapper สำหรับใช้เป็น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);
}
}
ตัวอย่าง:
string[] files = Directory.EnumerateFiles(@"C:\")
.OrderBy(f => f, new CustomComparer<string>(CompareNatural))
.ToArray();
นี่คือชุดชื่อไฟล์ที่ดีที่ฉันใช้ในการทดสอบ:
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();
โซลูชัน Pure C # สำหรับ 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;
}
}
คำตอบของ Matthews Horsleys เป็นวิธีที่เร็วที่สุดซึ่งจะไม่เปลี่ยนพฤติกรรมขึ้นอยู่กับว่าโปรแกรมของคุณกำลังทำงานบน windows รุ่นใด อย่างไรก็ตามสามารถทำได้เร็วยิ่งขึ้นโดยการสร้าง regex เพียงครั้งเดียวและใช้ RegexOptions.Compiled ฉันยังเพิ่มตัวเลือกในการแทรกตัวเปรียบเทียบสตริงเพื่อให้คุณสามารถละเว้นตัวพิมพ์เล็กและใหญ่ได้หากจำเป็นและปรับปรุงการอ่านให้ดีขึ้น
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);
}
ใช้โดย
var sortedEmployees = employees.OrderByNatural(emp => emp.Name);
สิ่งนี้ใช้เวลา 450ms ในการเรียง 100,000 สตริงเทียบกับ 300ms สำหรับการเปรียบเทียบสตริง. net เริ่มต้น - ค่อนข้างเร็ว!
วิธีแก้ปัญหาของฉัน:
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);
}
}
ผล:
1
a2
a3
a4
a10
b4
b5
b400
C1d
c1d2
คุณต้องระวัง - ฉันจำได้คร่าวๆว่าการอ่าน StrCmpLogicalW หรืออะไรทำนองนั้นไม่ใช่สกรรมกริยาอย่างเคร่งครัดและฉันสังเกตเห็นวิธีการเรียงลำดับของ. NET บางครั้งติดอยู่ในลูปที่ไม่มีที่สิ้นสุดหากฟังก์ชันการเปรียบเทียบทำลายกฎนั้น
การเปรียบเทียบสกรรมกริยาจะรายงานเสมอว่า a <c if a <b and b <c. มีฟังก์ชันที่ทำการเปรียบเทียบลำดับการเรียงตามธรรมชาติที่ไม่ตรงตามเกณฑ์นั้นเสมอไป แต่ฉันจำไม่ได้ว่าเป็น StrCmpLogicalW หรืออย่างอื่น
CultureInfo
มีคุณสมบัติCompareInfo
และวัตถุที่ส่งกลับมาสามารถจัดหาSortKey
วัตถุให้คุณได้ ในทางกลับกันสิ่งเหล่านี้สามารถเปรียบเทียบและรับประกันการขนส่งได้
นี่คือรหัสของฉันในการจัดเรียงสตริงที่มีทั้งอักขระอัลฟาและตัวเลข
ขั้นแรกวิธีการขยายนี้:
public static IEnumerable<string> AlphanumericSort(this IEnumerable<string> me)
{
return me.OrderBy(x => Regex.Replace(x, @"\d+", m => m.Value.PadLeft(50, '0')));
}
จากนั้นใช้ที่ใดก็ได้ในโค้ดของคุณดังนี้:
List<string> test = new List<string>() { "The 1st", "The 12th", "The 2nd" };
test = test.AlphanumericSort();
มันทำงานอย่างไร? โดยแทนที่ด้วยศูนย์:
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"
ทำงานร่วมกับตัวเลขทวีคูณ:
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"
หวังว่าจะช่วยได้
การเพิ่มคำตอบที่เกร็กบีชของ (เพราะฉันได้รับเพียงแค่การค้นหานั้น) ถ้าคุณต้องการที่จะใช้นี้จาก Linq คุณสามารถใช้ว่าจะใช้เวลาOrderBy
IComparer
เช่น:
var items = new List<MyItem>();
// fill items
var sorted = items.OrderBy(item => item.Name, new NaturalStringComparer());
นี่คือตัวอย่างที่ค่อนข้างง่ายที่ไม่ใช้ P / Invoke และหลีกเลี่ยงการจัดสรรใด ๆ ในระหว่างการดำเนินการ
internal sealed class NumericStringComparer : IComparer<string>
{
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;
}
}
มันไม่ได้ละเลย zeroes ชั้นนำเพื่อมาหลังจาก01
2
การทดสอบหน่วยที่เกี่ยวข้อง:
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));
}
}
ฉันได้ใช้มันเป็นวิธีการขยายStringComparer
เพื่อให้คุณสามารถทำได้เช่น:
StringComparer.CurrentCulture.WithNaturalSort()
หรือStringComparer.OrdinalIgnoreCase.WithNaturalSort()
.ส่งผลให้IComparer<string>
สามารถนำมาใช้ในสถานที่ที่ทุกคนชอบOrderBy
, OrderByDescending
, ThenBy
, ThenByDescending
, SortedSet<string>
ฯลฯ และคุณสามารถไวกรณีที่ยังคงปรับแต่งได้อย่างง่ายดายวัฒนธรรม ฯลฯ
การนำไปใช้นั้นค่อนข้างไม่สำคัญและควรทำงานได้ค่อนข้างดีแม้ในลำดับใหญ่ ๆ
ฉันยังเผยแพร่เป็นแพ็คเกจ NuGetขนาดเล็กดังนั้นคุณสามารถทำได้:
Install-Package NaturalSort.Extension
รหัสรวมถึงความคิดเห็นเอกสาร XML และชุดของการทดสอบที่มีอยู่ในNaturalSort.Extensionพื้นที่เก็บข้อมูล GitHub
รหัสทั้งหมดคือสิ่งนี้ (หากคุณยังไม่สามารถใช้ C # 7 ได้เพียงแค่ติดตั้งแพ็คเกจ NuGet):
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;
}
}
}
นี่คือวิธี LINQ แบบ regex-less แบบไร้เดียงสาบรรทัดเดียว (ยืมมาจาก 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"]
Dump()
. ขอบคุณที่ชี้ให้เห็น
เมื่อขยายคำตอบก่อนหน้านี้สองสามคำและการใช้วิธีการขยายฉันได้มาพร้อมกับคำตอบต่อไปนี้ที่ไม่มีข้อแม้ของการแจงนับที่อาจเกิดขึ้นได้หลายค่าหรือปัญหาด้านประสิทธิภาพที่เกี่ยวข้องกับการใช้วัตถุ regex หลายรายการหรือการเรียก regex โดยไม่จำเป็นว่า กล่าวคือใช้ ToList () ซึ่งสามารถลบล้างผลประโยชน์ในคอลเลกชันขนาดใหญ่ได้
ตัวเลือกรองรับการพิมพ์ทั่วไปเพื่ออนุญาตให้มอบหมายตัวแทนใด ๆ องค์ประกอบในคอลเลกชันที่มาจะถูกกลายพันธุ์โดยตัวเลือกจากนั้นแปลงเป็นสตริงด้วย 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);
}
ด้วยแรงบันดาลใจจากโซลูชันของ Michael Parker นี่คือการIComparer
ใช้งานที่คุณสามารถเลือกวิธีการสั่งซื้อ linq:
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);
}
}
เราต้องการการจัดเรียงที่เป็นธรรมชาติเพื่อจัดการกับข้อความด้วยรูปแบบต่อไปนี้:
"Test 1-1-1 something"
"Test 1-2-3 something"
...
ด้วยเหตุผลบางประการเมื่อฉันดู SO ครั้งแรกฉันไม่พบโพสต์นี้และนำไปใช้งานของเราเอง เมื่อเทียบกับโซลูชันบางอย่างที่นำเสนอที่นี่ในขณะที่มีแนวคิดคล้ายกัน แต่ก็อาจมีประโยชน์ที่อาจจะง่ายกว่าและเข้าใจง่ายกว่า อย่างไรก็ตามในขณะที่ฉันพยายามดูปัญหาคอขวดด้านประสิทธิภาพ แต่ก็ยังใช้งานได้ช้ากว่าค่าเริ่มต้นOrderBy()
มาก
นี่คือวิธีการขยายที่ฉันใช้:
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
}
}
}
แนวคิดคือการแยกสตริงเดิมออกเป็นบล็อกของตัวเลขและไม่ใช่ตัวเลข ( "\d+|\D+"
) เนื่องจากอาจเป็นงานที่มีราคาแพงจึงทำเพียงครั้งเดียวต่อรายการ จากนั้นเราใช้ตัวเปรียบเทียบของวัตถุที่เทียบเคียงได้ (ขออภัยฉันไม่พบวิธีที่เหมาะสมกว่านี้ในการพูด) จะเปรียบเทียบแต่ละบล็อกกับบล็อกที่เกี่ยวข้องในสตริงอื่น
ฉันต้องการความคิดเห็นเกี่ยวกับวิธีการปรับปรุงและข้อบกพร่องที่สำคัญคืออะไร โปรดทราบว่าความสามารถในการบำรุงรักษาเป็นสิ่งสำคัญสำหรับเราในตอนนี้และเราไม่ได้ใช้สิ่งนี้ในชุดข้อมูลขนาดใหญ่มาก
เวอร์ชันที่อ่าน / ดูแลรักษาง่ายกว่า
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;
}
}
ให้ฉันอธิบายปัญหาของฉันและฉันจะแก้ไขได้อย่างไร
ปัญหา: - เรียงไฟล์ตาม FileName จากอ็อบเจ็กต์ FileInfo ซึ่งดึงมาจากไดเร็กทอรี
วิธีแก้ไข: - ฉันเลือกชื่อไฟล์จาก FileInfo และตัดส่วน ".png" ของชื่อไฟล์ ตอนนี้เพียงแค่ทำ List.Sort () ซึ่งจะจัดเรียงชื่อไฟล์ตามลำดับการเรียงลำดับตามธรรมชาติ จากการทดสอบของฉันฉันพบว่าการมี. png ทำให้ลำดับการจัดเรียงยุ่งเหยิง ดูรหัสด้านล่าง
var imageNameList = new DirectoryInfo(@"C:\Temp\Images").GetFiles("*.png").Select(x =>x.Name.Substring(0, x.Name.Length - 4)).ToList();
imageNameList.Sort();