C# 字符串排序时指定偏好的排列顺序
独立观察员 2023 年 8 月 25 日
不知道大家有没有遇到过某些字符串数据在显示到界面上时需要按一定顺序排列的情况,如果内容是数值或字母自然好办,默认的排序功能就搞定了。那么如果是中文字符串呢?本文将会提供一个能在调用 OrderBy 方法时传入的字符串比较器,能够在一定程度上指定你偏爱的排列顺序,下面就一起来看看吧。
首先来看看原始数据:
List list1 = new List { "大 * 长", "中 * 长", "小 * 长", "大 * 宽", "中 * 宽", "小 * 宽", "大 * 高", "中 * 高", "小 * 高" }; | |
List list2 = new List { "酸 * 红", "甜 * 红", "苦 * 红", "辣 * 红", "酸 * 白", "甜 * 白", "苦 * 白", "辣 * 白", "酸 * 黄", "甜 * 黄", "苦 * 黄", "辣 * 黄" }; |
可以看出这两个字符串列表都是一些选项的排列组合,每一个字符串目前是由两个选项组合在一起。拿第一个列表来说,就是 “大、中、小” 与 “长、宽、高” 的排列组合。原始数据呈现的顺序是,第一个选项依次出现,第二个选项依次与第一个选项组合。
这样第一个需求就来了,也就是要求第一个选项的某一项与第二个选项全部组合过之后,再换下一项。这么说可能有点抽象,反正这个直接使用 OrderBy 就能实现了,所以直接给大家看结果吧:
// 大 * 高,大 * 宽,大 * 长,小 * 高,小 * 宽,小 * 长,中 * 高,中 * 宽,中 * 长 | |
string defaultOrder1 = string.Join(", ", list1.OrderBy(x => x)); | |
// 苦 * 白,苦 * 红,苦 * 黄,辣 * 白,辣 * 红,辣 * 黄,酸 * 白,酸 * 红,酸 * 黄,甜 * 白,甜 * 红,甜 * 黄 | |
string defaultOrder2 = string.Join(", ", list2.OrderBy(x => x)); | |
可以看到,通过 List 调用 OrderBy 方法就排好了(string.Join 只是把列表连接成字符串),效果就是 “大” 与 “长、宽、高” 都组合完之后才换下一项进行组合,这样看上去就更有条理一些。
但是,新的问题又显现出来了,就是 “大” 组合完之后,竟然是 “小” 进行组合,而不是符合常理的 “中” 参与组合;第二项也是 “高、宽、长” 而不是 “长、宽、高”。这个应该就是中文默认排序导致的,目测是按拼音首字母进行排序(“长” 可能被认为是 “zhang”)。那么如果我们想按照 大 -> 中 -> 小 这样的顺序进行排列,要怎么做呢?
实际上,OrderBy 方法除了第一个参数(lamda 表达式)之外,还有第二个参数(比较器):
也就是需要传一个 IComparer 泛型接口对象,来执行自定义的比较。
本人实现了一个 “字符串偏好比较器”,能够使用指定的偏好排序列表进行排序纠正,完整代码如下:
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Linq; | |
/* | |
* 源码己托管: https://gitee.com/dlgcy/dotnetcodes | |
*/ | |
namespace DotNet.Utilities | |
{ | |
/// <summary> | |
/// [dlgcy] 字符串偏好比较器 | |
/// </summary> | |
public class StringPreferenceComparer : IComparer<string> | |
{ | |
/// <summary> | |
/// 字符串中的分隔符 | |
/// </summary> | |
private readonly string _splitStr; | |
/// <summary> | |
/// 偏好的排序列表 | |
/// </summary> | |
public List<List<string>> _preferenceList; | |
/// <summary> | |
/// 构造函数 | |
/// </summary> | |
/// <param name="preferenceList">偏好的排序列表</param> | |
/// <param name="splitStr">字符串中的分隔符</param> | |
public StringPreferenceComparer(List<List<string>> preferenceList, string splitStr) | |
{ | |
_splitStr = splitStr; | |
_preferenceList = preferenceList ?? new List<List<string>>(); | |
} | |
/// <inheritdoc /> | |
public int Compare(string x, string y) | |
{ | |
if (!_preferenceList.Any()) | |
{ | |
return DefaultCompare(x, y); | |
} | |
var strsX = x?.Split(new[] { _splitStr }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>(); | |
var strsY = y?.Split(new[] { _splitStr }, StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<string>(); | |
if (!strsX.Any() && !strsY.Any()) | |
{ | |
return 0; | |
} | |
else if (strsX.Any() && !strsY.Any()) | |
{ | |
return 1; | |
} | |
else if (!strsX.Any() && strsY.Any()) | |
{ | |
return -1; | |
} | |
int countX = strsX.Length; | |
int countY = strsY.Length; | |
int index = 0; | |
while (index < countX && index < countY) | |
{ | |
string currentPartX = strsX[index]; | |
string currentPartY = strsY[index]; | |
List<string> matchedList = SearchMatchedList(currentPartX, currentPartY); | |
if (matchedList == null) | |
{ | |
return DefaultCompare(currentPartX, currentPartY); | |
} | |
int compareX = -1; | |
int compareY = -1; | |
for (int i = 0; i < matchedList.Count; i++) | |
{ | |
string str = matchedList[i]; | |
if (compareX == -1 && currentPartX.Contains(str)) | |
{ | |
compareX = i; | |
} | |
if (compareY == -1 && currentPartY.Contains(str)) | |
{ | |
compareY = i; | |
} | |
} | |
if (compareX != compareY) | |
{ | |
return compareX > compareY ? 1 : -1; | |
} | |
index++; | |
} | |
if (countX == countY) | |
{ | |
return DefaultCompare(x, y); | |
} | |
else if(countX < countY) | |
{ | |
return DefaultCompare(x, strsY.Take(countX).ToString()); | |
} | |
else | |
{ | |
return DefaultCompare(strsX.Take(countY).ToString(), y); | |
} | |
} | |
/// <summary> | |
/// 默认比较 | |
/// </summary> | |
private int DefaultCompare(string x, string y) | |
{ | |
return string.Compare(x, y, false, CultureInfo.CurrentCulture); | |
} | |
/// <summary> | |
/// 搜寻匹配的列表 | |
/// </summary> | |
/// <param name="currentPartX">X字符串的当前比较部分</param> | |
/// <param name="currentPartY">Y字符串的当前比较部分</param> | |
/// <returns></returns> | |
private List<string> SearchMatchedList(string currentPartX, string currentPartY) | |
{ | |
List<string> matchedList = null; | |
foreach (var list in _preferenceList) | |
{ | |
if (list.Exists(currentPartX.Contains) && list.Exists(currentPartY.Contains)) | |
{ | |
matchedList = list; | |
break; | |
} | |
} | |
return matchedList; | |
} | |
} | |
} |
为了显得更高深,我就没加注释了,哈哈。简单解释一下吧,也就是实现了 IComparer 接口,接口里有个方法 int Compare (string x, string y) 用于比较两个字符串。我添加了一个偏好排序列表以及一个分隔符字段,在构造函数中传入。在比较方法中,先使用分隔符,将两个字符串分别分割成多个部分,然后对于两者对应的部分,查找是否有适用的排序偏好列表,有的话,按照列表来排序,没有则使用默认的字符串排序。另外说一下 Compare 方法的规则:x > y 则需返回大于 0 的值(一般用 1),x < y 需返回小于 0 的值(一般用 -1),x == y 则返回 0。
然后看看如何使用吧:
List<List> preferenceList = new List<List>() | |
{ | |
new (){"大", "中", "小"}, | |
new (){"长", "宽", "高"}, | |
new (){"红", "黄", "白"}, | |
new (){"酸", "甜", "苦", "辣"}, | |
}; | |
var comparer = new StringPreferenceComparer(preferenceList, "*"); | |
// 大 * 长,大 * 宽,大 * 高,中 * 长,中 * 宽,中 * 高,小 * 长,小 * 宽,小 * 高 | |
string myOrder1 = string.Join(", ", list1.OrderBy(x => x, comparer)); | |
// 酸 * 红,酸 * 黄,酸 * 白,甜 * 红,甜 * 黄,甜 * 白,苦 * 红,苦 * 黄,苦 * 白,辣 * 红,辣 * 黄,辣 * 白 | |
string myOrder2 = string.Join(", ", list2.OrderBy(x => x, comparer)); |
可以看到最终结果已经按照给定的排序偏好进行排序了。
整个单元测试代码如下,可以整体对比一下:
最后,说明一下,本方法实现了本次需求,其余的情况应该也是可以的,不过未进行测试,大家可以自行测试,也欢迎提出修改建议。
代码托管:https://gitee.com/dlgcy/dotnetcodes
https://gitee.com/dlgcy/dotnetcodes/blob/dlgcy/DotNet.Utilities/%E5%AD%97%E7%AC%A6%E4%B8%B2/StringPreferenceComparer.cs
全文完,感谢阅读。