2009-06-23 16 views
11

Tengo una lista de cadenas que pueden contener una letra o una cadena de una int (max 2 dígitos). Deben ordenarse alfabéticamente o (cuando en realidad es un int) en el valor numérico que representa.Ordenando números y cadenas mixtas

Ejemplo:

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

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

Lo que yo quiero es

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

tengo una idea que implica formatearlo con tratar de analizarlo, a continuación, si es un TryParse exitosa para formatearlo con mi propio formato de cadena personalizado para que tenga ceros precedentes. Espero algo más simple y eficiente.

Editar
terminé haciendo un IComparer Me deshice de mi biblioteca Utilidades para su uso posterior.
Mientras estaba en eso, lancé dobles en la mezcla también.

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

Respuesta

12

Quizás podría ir con un enfoque más genérico y usar un algoritmo natural sorting como la implementación de C# here.

+0

genial.lo hubiera usado si se hubiera hecho antes: P –

+1

Muy bien, acabo de encontrar un envoltorio Delphi para esto también http://irsoft.de/web/strnatcmp-and-natsort-for-delphi –

+0

Esto no funcionará en todos los casos . Suponga que ypu tiene la siguiente lista de elementos: "0/30" "0/248" "0/496" "0/357.6". Esta orden será guardada después de la clasificación, que no es lo que puede esperar. –

2

yo diría que se podía dividir los valores mediante una RegularExpression (suponiendo que todo es un int) y luego reunirse con ellos juntos.

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

Luego, con sus dos listas puede ordenar cada una de ellas y luego fusionarlas nuevamente en el formato que desee.

+0

Sí, esto es más simple que mi enfoque. +1 –

3

Utilice la otra sobrecarga de OrderBy que toma un parámetro IComparer.

Puede implementar su propio IComparer que usa int.TryParse para saber si es un número o no.

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

Aún realiza la conversión, pero solo una vez/artículo.

17

Me vienen a la mente dos formas, no estoy seguro de cuál es el rendimiento más alto. Implementar un IComparer personalizado:

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

O, dividir la secuencia en números y no los números, a continuación, ordenar cada subgrupo, finalmente unirse a los resultados ordenados; algo así como:

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)); 
+0

Su IComparer no devuelve cadenas no numéricas en el orden correcto (alfabético). Tu consulta LINQ sí. – LukeH

+0

Sí, gracias, arreglaré eso. – LBushkin

+0

Agregué mi código de finalización en el OP. También noté lo de la cuerda. Además probé el cortocircuito antes de cada análisis. No sé si ofrece mucho rendimiento, pero me llevó exactamente el mismo esfuerzo reordenarlos, ya que me hubiera llevado a probarlo;) –

1

Se puede usar un comparador de encargo - la declaración de pedido sería entonces:

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

donde MyComparer se define así:

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

Aunque esto puede parecer una bit verbose, encapsula la lógica de clasificación en un tipo apropiado. Luego, si lo desea, puede someter fácilmente el Comparador a pruebas automatizadas (pruebas unitarias). También es reutilizable.

(Puede ser posible hacer el algoritmo un poco más claro, pero esto era lo mejor que podía tirar rápidamente juntos.)

0

Se podría también "trampa" en algún sentido.Según su descripción del problema, sabe que cualquier cadena de longitud 2 será un número. Así que simplemente ordena todas las cadenas de longitud 1. Y luego ordena todas las cadenas de longitud 2. Y luego haz un montón de cambios para reordenar tus cadenas en el orden correcto. Esencialmente, el proceso funcionará de la siguiente manera: (suponiendo que sus datos están en una matriz)

Paso 1: Empuje todas las cadenas de longitud 2 hasta el final de la matriz. Hacer un seguimiento de cuántos tienes.

Paso 2: En lugar de ordenar las cuerdas de longitud 1 y cadenas de longitud 2.

Paso 3: La búsqueda binaria para 'a' lo que sería en el límite de sus dos mitades.

Paso 4: Cambie las cadenas de dos dígitos con las letras según sea necesario.

Dicho esto, aunque este enfoque funcionará, no implica expresiones regulares, y no intenta analizar valores no-int como int - No lo recomiendo. Escribirás significativamente más código que otros enfoques ya sugeridos. Ofusca el punto de lo que estás tratando de hacer. No funciona si de repente se obtienen cadenas de dos letras o cadenas de tres dígitos. Etc. Solo lo estoy incluyendo para mostrar cómo se pueden ver los problemas de manera diferente y encontrar soluciones alternativas.

2

Se podía utilizar la función provided by the Win32 API:

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

y llamarlo desde una IComparer como otros han mostrado.

1

¡Usa un Schwartzian Transform para realizar O (n) conversiones!

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

No es más sencillo que lo que publiqué para todo lo que sé. Podría ser más eficiente, pero no es lo suficientemente crítico como para poner el rendimiento por encima de la simplicidad –

0

Tuve un problema similar y aterrizó aquí: ordenando cadenas que tienen un sufijo numérico como en el siguiente ejemplo.

original: resultado especie

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

defecto:

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

deseado resultado para ordenar:

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

acabé utilizando un comparador personalizado:

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)

Cuestiones relacionadas