按数字顺序对字母数字字符串进行排序,然后按前缀/后缀排序

时间:2018-09-24 15:16:10

标签: c# .net linq sorting

我有一个复杂的排序模式可以复制,我的解决方案似乎有些牵强。 我的输入是一个数字列表,该数字可以有多个字母作为后缀,前缀都只是字母(“ aaa”,“ aab”,“ ac”等)。 我需要按数字排序,然后按后缀(如果有的话)排序,然后按前缀(如果有的话)排序。

例如

"a1a",
"5ac",
"1",
"12",
"2",
"11",
"5aa",
"3",
"5ab",
"a2b",
"abb11ca",
"1b",
"aba11ca"

将被排序为

1
a1a
1b
2
a2b
3
5aa
5ab
5ac
11
aba11ca
abb11ca
12

这是我使用Linq想到的解决方案。

static void Main(string[] args)
{
    var arr = new []
    {"b2","a1a","5ac","1","12","2","11","5aa","3","5ab","a1","a2b","abb11ca","1b","aba11ca"
    };
    var ordered = arr.Select(str => {
                var parts = SplitIntoPrefixNumberSuffix(str);
                var number = int.Parse(parts[1]);
                return new { str, parts, number };
            })
            .OrderBy(x => x.number).ThenBy(x => x.parts[2]).ThenBy(x => x.parts[0])
            .Select(x => x.str);

    Console.WriteLine("sorted array: ");
    foreach (var s in ordered)
    {
        Console.WriteLine("{0}", s);
    }

    Console.ReadLine();
}      

public static string[] SplitIntoPrefixNumberSuffix(string str)
{
    var numChar = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
    var numLoc = str.IndexOfAny(numChar);
    var nums = "";
    foreach (var c in str)
    {
        if (char.IsDigit(c))
            nums = nums + c;
    }
    Console.WriteLine("numLoc: {0}; nums: {1}", numLoc, nums.Count());
    var prefix = str.Substring(0, numLoc);
    var suffix = str.Substring(numLoc + nums.Count());
    Console.WriteLine("prefix {0}; nums {1}; suffix {2}", prefix, nums, suffix);
    return new[] { prefix, nums, suffix };
}

以下是它的.netfiddle文件:https://dotnetfiddle.net/C7ZA0b

虽然有效,但感觉它不是一个很好的解决方案。我要遍历整个集合几次,我认为我应该使用自定义可比对象。

我以前从未写过可比书;我查看了Dot Net Pearls Alphanumeric Sorting,可以关注它,但对其进行修改不足以满足我的需求。

我可以使用IComparable来完成上述工作吗?在学习如何写作的好地方有什么建议吗?

4 个答案:

答案 0 :(得分:3)

因此,您可以使用以正则表达式命名的组来分割字符串的各个组成部分,然后按每个组成部分进行排序:

var regex = new Regex(@"^(?<pre>\D*)(?<num>\d+)(?<suff>\D*)$");
var ordered = data.Select(d => (match: regex.Match(d), value: d))
    .Where(x => x.match.Success) //throw away anything that doesn't conform
    .Select(x => (
        x.value, 
        pre: x.match.Groups["pre"].Value, 
        num: int.Parse(x.match.Groups["num"].Value), 
        suff: x.match.Groups["suff"].Value))
    .OrderBy(x => x.num)
    .ThenBy(x => x.suff)
    .ThenBy(x => x.pre)
    .Select(x => x.value);

...但是最终,这与您的解决方案并没有什么不同。我真的看不到专门的IComparer如何简化这一过程。

如果没有可用的元组(

data.Select(d => new { match = regex.Match(d), value = d})
    .Where(x => x.match.Success)
    .Select(x => new { 
        x.value, 
        pre = x.match.Groups["pre"].Value, 
        num = int.Parse(x.match.Groups["num"].Value), 
        suff = x.match.Groups["suff"].Value})
    .OrderBy(x => x.num)
    .ThenBy(x => x.suff)
    .ThenBy(x => x.pre)
    .Select(x => x.value)

答案 1 :(得分:1)

作为选项,您可以实现自己的自定义比较器

public class CustomStringComparer : IComparer<string>
{
    public int Compare(string first, string second)
    {
        var compareByCore = CompareCore(first, second);
        var compareBySuffix = CompareSuffix(first, second);
        var compareByPrefix = ComparePrefix(first, second);

        return compareByCore != 0 ? compareByCore 
            : compareBySuffix != 0 ? compareBySuffix
            : compareByPrefix;
    }

    private int CompareCore(string a, string b)
    {
        var firstCoreNumber = Regex.Match(a, @"\d+").Value;
        var secondCoreNumber = Regex.Match(b, @"\d+").Value;

        if (!string.IsNullOrEmpty(firstCoreNumber) && !string.IsNullOrEmpty(secondCoreNumber))
        {
            return int.Parse(firstCoreNumber).CompareTo(int.Parse(secondCoreNumber));
        }

        return 0;
    }

    private int CompareSuffix(string a, string b)
    {
        var firstSuffix = Regex.Match(a, @"\D+$").Value;
        var secondSuffix = Regex.Match(b, @"\D+$").Value;

        return firstSuffix.CompareTo(secondSuffix);
    }

    private int ComparePrefix(string a, string b)
    {
        var firstPrefix = Regex.Match(a, @"^\D+").Value;
        var secondPrefix = Regex.Match(b, @"^\D+").Value;

        return firstPrefix.CompareTo(secondPrefix);
    }
}

当您调用order方法时,只需发送此比较器的一个实例:

var arr = new[]
        { "a1a", "5ac", "1", "12", "2", "11", "5aa", "3", "5ab", "a2b", "abb11ca", "1b", "aba11ca" };

    var sortedArr = arr.OrderBy(x => x, new CustomStringComparer());

    foreach (var s in sortedArr)
    {
        Console.Write($"{s} ");
    }

答案 2 :(得分:0)

作为我现有答案的替代方法,我不久前写了一个ComparerBuilder<T>帮助程序,可以编写一些不错的客户端代码。

现在您可以:

var comparer = new ComparerBuilder<string>()
    .SortKey(k => int.Parse(Regex.Match(k, @"\d+").Value))
        .ThenKey(k => Regex.Match(k, @"\D*$").Value)
        .ThenKey(k => Regex.Match(k, @"^\D*").Value)
        .Build();

var ordered = data.OrderBy(x => x, comparer);

答案 3 :(得分:-1)

这里是implementationIComparer Dave Koelle字母数字。

编辑:添加代码示例。

void Main()
{
    var arr = new[] {"b2","a1a","5ac","1","12","2","11","5aa","3","5ab","a1","a2b","abb11ca","1b","aba11ca" };

    var items = arr.OrderBy(x => x.ToString(), new AlphanumComparator()).ToList();

    Console.WriteLine("sorted array: ");
    foreach (var s in items)
    {
        Console.WriteLine("{0}", s);
    }
}

public class AlphanumComparator : IComparer<object>
{
    private enum ChunkType { Alphanumeric, Numeric };
    private bool InChunk(char ch, char otherCh)
    {
        ChunkType type = ChunkType.Alphanumeric;

        if (char.IsDigit(otherCh))
        {
            type = ChunkType.Numeric;
        }

        if ((type == ChunkType.Alphanumeric && char.IsDigit(ch))
            || (type == ChunkType.Numeric && !char.IsDigit(ch)))
        {
            return false;
        }

        return true;
    }

    public int Compare(object x, object y)
    {
        String s1 = x as string;
        String s2 = y as string;
        if (s1 == null || s2 == null)
        {
            return 0;
        }

        int thisMarker = 0, thisNumericChunk = 0;
        int thatMarker = 0, thatNumericChunk = 0;

        while ((thisMarker < s1.Length) || (thatMarker < s2.Length))
        {
            if (thisMarker >= s1.Length)
            {
                return -1;
            }
            else if (thatMarker >= s2.Length)
            {
                return 1;
            }
            char thisCh = s1[thisMarker];
            char thatCh = s2[thatMarker];

            StringBuilder thisChunk = new StringBuilder();
            StringBuilder thatChunk = new StringBuilder();

            while ((thisMarker < s1.Length) && (thisChunk.Length == 0 || InChunk(thisCh, thisChunk[0])))
            {
                thisChunk.Append(thisCh);
                thisMarker++;

                if (thisMarker < s1.Length)
                {
                    thisCh = s1[thisMarker];
                }
            }

            while ((thatMarker < s2.Length) && (thatChunk.Length == 0 || InChunk(thatCh, thatChunk[0])))
            {
                thatChunk.Append(thatCh);
                thatMarker++;

                if (thatMarker < s2.Length)
                {
                    thatCh = s2[thatMarker];
                }
            }

            int result = 0;
            // If both chunks contain numeric characters, sort them numerically
            if (char.IsDigit(thisChunk[0]) && char.IsDigit(thatChunk[0]))
            {
                thisNumericChunk = Convert.ToInt32(thisChunk.ToString());
                thatNumericChunk = Convert.ToInt32(thatChunk.ToString());

                if (thisNumericChunk < thatNumericChunk)
                {
                    result = -1;
                }

                if (thisNumericChunk > thatNumericChunk)
                {
                    result = 1;
                }
            }
            else
            {
                result = thisChunk.ToString().CompareTo(thatChunk.ToString());
            }

            if (result != 0)
            {
                return result;
            }
        }

        return 0;
    }
}