Сортировка смешанных чисел и строк

У меня есть список строк, которые могут содержать букву или строковое представление int (max 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, чтобы он имел предшествующие нули. Я надеюсь на что-то более простое и результативное.

Edit
Я закончил создание 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);
}

Ответ 2

Приходят на ум два способа, а не уверен, что они более эффективны. Внедрение пользовательского 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 ) );

Ответ 3

Используйте другую перегрузку OrderBy, которая принимает параметр IComparer.

Затем вы можете реализовать свой собственный IComparer, который использует int.TryParse, чтобы указать, является ли это числом или нет.

Ответ 4

Я бы сказал, что вы можете разделить значения, используя регулярное выражение (если все есть 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);
    }
}

Затем с помощью двух списков вы можете отсортировать каждый из них, а затем объединить их обратно в любом формате.

Ответ 5

Вы можете просто использовать функцию предоставленную Win32 API:

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

и назовите его с помощью IComparer, как показали другие.

Ответ 6

Вы можете использовать пользовательский сопоставитель - оператор упорядочения будет следующим:

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

Используйте 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));
}

Ответ 8

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);
}

Он по-прежнему выполняет преобразование, но только один раз/элемент.

Ответ 9

Вы также можете "обмануть" в некотором смысле. Основываясь на вашем описании проблемы, вы знаете, что любая строка длины 2 будет числом. Поэтому просто отсортируйте все строки длины 1. И затем отсортируйте все строки длины 2. И затем сделайте кучу замены, чтобы переупорядочить ваши строки в правильном порядке. По существу, процесс будет работать следующим образом: (если ваши данные находятся в массиве.)

Шаг 1: Нажимаем все строки длиной 2 до конца массива. Отслеживание того, сколько у вас есть.

Шаг 2: Разместите строки Строки длины 1 и Строки длины 2.

Шаг 3: Двоичный поиск "a", который будет находиться на границе ваших двух половин.

Шаг 4: Поменяйте свои две строки с буквами при необходимости.

Тем не менее, хотя этот подход будет работать, не требует регулярных выражений и не пытается анализировать значения non-int как int - я не рекомендую его. Вы будете писать значительно больше кода, чем другие предложенные ранее подходы. Это смущает то, что вы пытаетесь сделать. Это не работает, если вы вдруг получите две строки букв или трехзначные строки. И т.д. Я просто включаю его, чтобы показать, как вы можете смотреть на проблемы по-другому, и придумать альтернативные решения.

Ответ 10

У меня была аналогичная проблема и приземлился здесь: сортировка строк с числовым суффиксом, как в следующем примере.

Оригинал:

"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)