对混合数字和字符串进行排序

时间:2009-06-23 14:06:04

标签: c# sorting formatting tostring

我有一个字符串列表,可以包含一个字母或一个int的字符串表示(最多2位数)。 它们需要按字母顺序排序,或者(当它实际上是一个int时)对它所代表的数值进行排序。

示例:

IList<string> input = new List<string>()
    {"a", 1.ToString(), 2.ToString(), "b", 10.ToString()};

input.OrderBy(s=>s)
  // 1
  // 10
  // 2
  // a
  // b

我想要的是

  // 1
  // 2
  // 10
  // a
  // b

我有一些想法涉及通过尝试解析它来格式化它,然后如果它是一个成功的tryparse用我自己的自定义stringformatter格式化它使它有前面的零。我希望能有更简单,更高效的东西。

修改
我最终制作了一个IComparer,我把它放在我的Utils库中供以后使用 当我在场时,我也把混合物扔进了混合物中。

public class MixedNumbersAndStringsComparer : IComparer<string> {
    public int Compare(string x, string y) {
        double xVal, yVal;

        if(double.TryParse(x, out xVal) && double.TryParse(y, out yVal))
            return xVal.CompareTo(yVal);
        else 
            return string.Compare(x, y);
    }
}

//Tested on int vs int, double vs double, int vs double, string vs int, string vs doubl, string vs string.
//Not gonna put those here
[TestMethod]
public void RealWorldTest()
{
    List<string> input = new List<string>() { "a", "1", "2,0", "b", "10" };
    List<string> expected = new List<string>() { "1", "2,0", "10", "a", "b" };
    input.Sort(new MixedNumbersAndStringsComparer());
    CollectionAssert.AreEquivalent(expected, input);
}

10 个答案:

答案 0 :(得分:20)

我想到了两种方式,不确定哪种方式更具性能。实现自定义IComparer:

class MyComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        int xVal, yVal;
        var xIsVal = int.TryParse( x, out xVal );
        var yIsVal = int.TryParse( y, out yVal );

        if (xIsVal && yIsVal)   // both are numbers...
            return xVal.CompareTo(yVal);
        if (!xIsVal && !yIsVal) // both are strings...
            return x.CompareTo(y);
        if (xIsVal)             // x is a number, sort first
            return -1;
        return 1;               // x is a string, sort last
    }
}

var input = new[] {"a", "1", "10", "b", "2", "c"};
var e = input.OrderBy( s => s, new MyComparer() );

或者,将序列拆分为数字和非数字,然后对每个子组进行排序,最后加入排序结果;类似的东西:

var input = new[] {"a", "1", "10", "b", "2", "c"};

var result = input.Where( s => s.All( x => char.IsDigit( x ) ) )
                  .OrderBy( r => { int z; int.TryParse( r, out z ); return z; } )
                  .Union( input.Where( m => m.Any( x => !char.IsDigit( x ) ) )
                               .OrderBy( q => q ) );

答案 1 :(得分:12)

也许您可以使用更通用的方法并使用natural sorting算法,例如C#实现here

答案 2 :(得分:3)

使用带有OrderBy参数的IComparer的其他重载。

然后,您可以实施自己使用IComparer的{​​{1}}来判断它是否为数字。

答案 3 :(得分:2)

我会说你可以使用RegularExpression拆分值(假设一切都是int),然后将它们重新加入。

//create two lists to start
string[] data = //whatever...
List<int> numbers = new List<int>();
List<string> words = new List<string>();

//check each value
foreach (string item in data) {
    if (Regex.IsMatch("^\d+$", item)) {
        numbers.Add(int.Parse(item));
    }
    else {
        words.Add(item);
    }
}

然后使用您的两个列表,您可以对它们进行排序,然后以您想要的任何格式将它们合并在一起。

答案 4 :(得分:2)

你可以使用函数provided by the Win32 API

[DllImport ("shlwapi.dll", CharSet=CharSet.Unicode, ExactSpelling=true)]
static extern int StrCmpLogicalW (String x, String y);

并像其他人一样从IComparer拨打电话。

答案 5 :(得分:1)

public static int? TryParse(string s)
{
    int i;
    return int.TryParse(s, out i) ? (int?)i : null;
}

// in your method
IEnumerable<string> input = new string[] {"a", "1","2", "b", "10"};
var list = input.Select(s => new { IntVal = TryParse(s), String =s}).ToList();
list.Sort((s1, s2) => {
    if(s1.IntVal == null && s2.IntVal == null)
    {
        return s1.String.CompareTo(s2.String);
    }
    if(s1.IntVal == null)
    {
        return 1;
    }
    if(s2.IntVal == null)
    {
        return -1;
    }
    return s1.IntVal.Value.CompareTo(s2.IntVal.Value);
});
input = list.Select(s => s.String);

foreach(var x in input)
{
    Console.WriteLine(x);
}

它仍然进行转换,但只进行一次/项目。

答案 6 :(得分:1)

您可以使用自定义比较器 - 订购声明将是:

var result = input.OrderBy(s => s, new MyComparer());

其中MyComparer的定义如下:

public class MyComparer : Comparer<string>
{
    public override int Compare(string x, string y)
    {

        int xNumber;
        int yNumber;
        var xIsNumber = int.TryParse(x, out xNumber);
        var yIsNumber = int.TryParse(y, out yNumber);

        if (xIsNumber && yIsNumber)
        {
            return xNumber.CompareTo(yNumber);
        }
        if (xIsNumber)
        {
            return -1;
        }
        if (yIsNumber)
        {
            return 1;
        }
        return x.CompareTo(y);
    }
}

虽然这看起来有点冗长,但它将排序逻辑封装成适当的类型。如果您愿意,您可以轻松地使Comparer进行自动化测试(单元测试)。它也是可重复使用的。

(有可能使算法更清晰一点,但这是我能迅速拼凑起来的最佳方法。)

答案 7 :(得分:1)

你也可以在某种意义上“欺骗”。根据您对问题的描述,您知道任何长度为2的字符串都是数字。所以只需对长度为1的所有字符串进行排序。然后对长度为2的所有字符串进行排序。然后进行一堆交换,以正确的顺序重新排序字符串。本质上,该过程将按如下方式工作:(假设您的数据位于数组中。)

步骤1:将长度为2的所有字符串推送到数组的末尾。跟踪你有多少。

步骤2:在适当的位置对长度为1的字符串和长度为2的字符串进行排序。

第3步:二元搜索“a”,它将位于你们两半的边界上。

步骤4:根据需要用字母交换两位数的字符串。

尽管如此,虽然这种方法可行,但不涉及正则表达式,并且不会尝试将非int值解析为int - 我不推荐它。您将编写比已建议的其他方法更多的代码。它模糊了你想要做的事情的重点。如果你突然得到两个字母的字符串或三个数字字符串,它不起作用。等等。我只是把它包括在内,以展示你如何以不同的方式看待问题,并提出替代解决方案。

答案 8 :(得分:1)

使用Schwartzian Transform执行O(n)转换!

private class Normalized : IComparable<Normalized> {
  private readonly string str;
  private readonly int val;

  public Normalized(string s) {
    str = s;

    val = 0;
    foreach (char c in s) {
      val *= 10;

      if (c >= '0' && c <= '9')
        val += c - '0';
      else
        val += 100 + c;
    }
  }

  public String Value { get { return str; } }

  public int CompareTo(Normalized n) { return val.CompareTo(n.val); }
};

private static Normalized In(string s) { return new Normalized(s); }
private static String Out(Normalized n) { return n.Value; }

public static IList<String> MixedSort(List<String> l) {
  var tmp = l.ConvertAll(new Converter<String,Normalized>(In));
  tmp.Sort();
  return tmp.ConvertAll(new Converter<Normalized,String>(Out));
}

答案 9 :(得分:1)

我遇到了类似的问题,并在此处着陆:排序具有数字后缀的字符串,如下例所示。

原文:

"Test2", "Test1", "Test10", "Test3", "Test20"

默认排序结果:

"Test1", "Test10", "Test2", "Test20", "Test3"

所需的排序结果:

"Test1", "Test2", "Test3, "Test10", "Test20"

我最终使用了自定义Comparer:

public class NaturalComparer : IComparer
{

    public NaturalComparer()
    {
        _regex = new Regex("\\d+$", RegexOptions.IgnoreCase);
    }

    private Regex _regex;

    private string matchEvaluator(System.Text.RegularExpressions.Match m)
    {
        return Convert.ToInt32(m.Value).ToString("D10");
    }

    public int Compare(object x, object y)
    {
        x = _regex.Replace(x.ToString, matchEvaluator);
        y = _regex.Replace(y.ToString, matchEvaluator);

        return x.CompareTo(y);
    }
}   

HTH; o)