¿Cómo ordeno las cadenas alfabéticamente mientras considero el valor cuando una cadena es numérica?

100

Estoy tratando de ordenar una matriz de números que son cadenas y me gustaría que se ordenaran numéricamente.

El problema es que no puedo convertir los números en int .

Aquí está el código:

string[] things= new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => x))
{
    Console.WriteLine(thing);
}

salida: 101, 102, 103, 105, 90

Me gustaría: 90, 101, 102, 103, 105

EDITAR: La salida no puede ser 090, 101, 102 ...

Se actualizó la muestra de código para que diga "cosas" en lugar de "tamaños". La matriz puede ser algo como esto:

string[] things= new string[] { "paul", "bob", "lauren", "007", "90" };

Eso significa que debe ordenarse alfabéticamente y por número:

007, 90, bob lauren, paul

sf.
fuente
8
¿Por qué no puedes convertirlos a int?
Femaref
1
"tamaños" puede ser algo más como "nombre". La muestra de código simplemente está simplificada.
sf.
2
¿Alguno de los números será negativo? ¿Serán todos números enteros? ¿Cuál es el rango de los números enteros?
Eric Lippert
"cosas" puede ser cualquier tipo de cadena. Me gustaría que la lista estuviera ordenada de manera lógica para una persona que no tenga conocimientos de informática. Los números negativos deben ir antes del positivo. En cuanto a la longitud de la cadena, no tendrá más de 100 caracteres.
sf.
5
Que tan lejos quieres ir? ¿Debería image10venir después image2? ¿Debería Januaryvenir antes February?
svick

Respuestas:

104

Pase un comparador personalizado a OrderBy. Enumerable.OrderBy le permitirá especificar cualquier comparador que desee.

Esta es una forma de hacerlo:

void Main()
{
    string[] things = new string[] { "paul", "bob", "lauren", "007", "90", "101"};

    foreach (var thing in things.OrderBy(x => x, new SemiNumericComparer()))
    {    
        Console.WriteLine(thing);
    }
}


public class SemiNumericComparer: IComparer<string>
{
    /// <summary>
    /// Method to determine if a string is a number
    /// </summary>
    /// <param name="value">String to test</param>
    /// <returns>True if numeric</returns>
    public static bool IsNumeric(string value)
    {
        return int.TryParse(value, out _);
    }

    /// <inheritdoc />
    public int Compare(string s1, string s2)
    {
        const int S1GreaterThanS2 = 1;
        const int S2GreaterThanS1 = -1;

        var IsNumeric1 = IsNumeric(s1);
        var IsNumeric2 = IsNumeric(s2);

        if (IsNumeric1 && IsNumeric2)
        {
            var i1 = Convert.ToInt32(s1);
            var i2 = Convert.ToInt32(s2);

            if (i1 > i2)
            {
                return S1GreaterThanS2;
            }

            if (i1 < i2)
            {
                return S2GreaterThanS1;
            }

            return 0;
        }

        if (IsNumeric1)
        {
            return S2GreaterThanS1;
        }

        if (IsNumeric2)
        {
            return S1GreaterThanS2;
        }

        return string.Compare(s1, s2, true, CultureInfo.InvariantCulture);
    }
}
Jeff Paulsen
fuente
1
Para la entrada dada, esto produce el mismo resultado que la respuesta de Recursive, que involucra PadLeft (). Supongo que su entrada es en realidad más compleja de lo que muestra este ejemplo, en cuyo caso un comparador personalizado es el camino a seguir.
Jeff Paulsen
Salud. Esta solución funciona y parece una forma limpia y fácil de leer de implementar. +1 por mostrarme que puede usar IComparer en OrderBy :)
sf.
17
El IsNumericmétodo es malo, una codificación impulsada por excepciones siempre es mala. Úselo en su int.TryParselugar. Pruebe su código con una lista grande y le llevará una eternidad.
Nean Der Thal
Si es útil, agregué una extensión a esta versión aquí que agrega soporte para ordenar con palabras. Para mis necesidades, la división en espacios era suficiente, y tenía poca necesidad de preocuparme por las palabras de uso mixto (por ejemplo, test12 vs test3),
matt.bungard
@NeanDerThal Estoy bastante seguro de que solo es lento / malo el manejo de muchas excepciones en un bucle, si está depurando o está accediendo al objeto Exception.
Kelly Elton
90

Simplemente rellene con ceros de la misma longitud:

int maxlen = sizes.Max(x => x.Length);
var result = sizes.OrderBy(x => x.PadLeft(maxlen, '0'));
recursivo
fuente
+1 para una solución simple, quisquilloso (ya hecho en la edición, bueno)
Marino Šimić
Buena idea, pero el siguiente problema es que necesito mostrar estos valores para que el "90" sea un "90", no un "090"
sf.
6
@sf: Pruébelo, puede que le guste el resultado. Recuerde, la clave de pedido no es lo que se solicita. Si digo que ordene una lista de clientes por apellido, entonces obtengo una lista de clientes, no una lista de apellidos. Si dice ordenar una lista de cadenas por una cadena transformada, el resultado es la lista ordenada de cadenas originales, no cadenas transformadas.
Eric Lippert
Tuve que agregar "tamaños = tamaños.OrderBy (...)" para que esto funcione. ¿Es eso normal o debería editarse la respuesta?
gorgabal
1
@gorgabal: En general, reasignar a sizestampoco funcionaría, porque el resultado es de un tipo diferente. La respuesta es un tanto abreviada, ya que la segunda línea muestra el resultado como una expresión, pero depende del lector hacer algo con él. Agregué otra asignación de variable para que quede más claro.
recursivo
74

Y, que tal esto ...

string[] sizes = new string[] { "105", "101", "102", "103", "90" };

var size = from x in sizes
           orderby x.Length, x
           select x;

foreach (var p in size)
{
    Console.WriteLine(p);
}
shenhengbin
fuente
jeje, realmente me gusta este, muy inteligente. Lo siento si no proporcioné el conjunto completo de datos iniciales
sf.
3
Esto es como la opción de almohadilla anterior, solo que en mi opinión mucho mejor.
dudeNumber4
3
var tamaño = tamaños.OrderBy (x => x.Length) .ThenBy (x => x);
Phillip Davis
1
Pero esto va a mezclar cadenas alfabéticas como este: "b", "ab", "101", "103", "bob", "abcd".
Andrew
67

El valor es una cadena

List = List.OrderBy(c => c.Value.Length).ThenBy(c => c.Value).ToList();

Trabajos

Eco
fuente
2
Esta respuesta es mi favorita.
LacOniC
2
Gracias. Acabo de descubrir que sale del método "ThenBy".
ganchito55
Esto funciona muy bien para mi caso de uso, donde la entrada está en el formato nuevostring[] { "Object 1", "Object 9", "Object 14" }
thelem
2
Esta es la mejor respuesta. Funciona y es un buen aprendizaje. Gracias !!
Julio
1
Pero esto va a mezclar cadenas alfabéticas como este: "b", "ab", "101", "103", "bob", "abcd".
Andrew
13

Hay una función nativa en Windows StrCmpLogicalWque comparará en cadenas números como números en lugar de letras. Es fácil hacer un comparador que llame a esa función y la use para sus comparaciones.

public class StrCmpLogicalComparer : Comparer<string>
{
    [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
    private static extern int StrCmpLogicalW(string x, string y);

    public override int Compare(string x, string y)
    {
        return StrCmpLogicalW(x, y);
    }
}

Incluso funciona en cadenas que tienen texto y números. Aquí hay un programa de ejemplo que mostrará la diferencia entre la clasificación predeterminada y la StrCmpLogicalWclasificación

class Program
{
    static void Main()
    {
        List<string> items = new List<string>()
        {
            "Example1.txt", "Example2.txt", "Example3.txt", "Example4.txt", "Example5.txt", "Example6.txt", "Example7.txt", "Example8.txt", "Example9.txt", "Example10.txt",
            "Example11.txt", "Example12.txt", "Example13.txt", "Example14.txt", "Example15.txt", "Example16.txt", "Example17.txt", "Example18.txt", "Example19.txt", "Example20.txt"
        };

        items.Sort();

        foreach (var item in items)
        {
            Console.WriteLine(item);
        }

        Console.WriteLine();

        items.Sort(new StrCmpLogicalComparer());

        foreach (var item in items)
        {
            Console.WriteLine(item);
        }
        Console.ReadLine();
    }
}

que salidas

Example1.txt
Example10.txt
Example11.txt
Example12.txt
Example13.txt
Example14.txt
Example15.txt
Example16.txt
Example17.txt
Example18.txt
Example19.txt
Example2.txt
Example20.txt
Example3.txt
Example4.txt
Example5.txt
Example6.txt
Example7.txt
Example8.txt
Example9.txt

Example1.txt
Example2.txt
Example3.txt
Example4.txt
Example5.txt
Example6.txt
Example7.txt
Example8.txt
Example9.txt
Example10.txt
Example11.txt
Example12.txt
Example13.txt
Example14.txt
Example15.txt
Example16.txt
Example17.txt
Example18.txt
Example19.txt
Example20.txt
Scott Chamberlain
fuente
Ojalá fuera más fácil usar las bibliotecas del sistema en C #
Kyle Delaney
Esto habría sido perfecto, pero desafortunadamente no maneja números negativos. -1 0 10 2está ordenado como0 -1 2 10
nphx
5

prueba esto

sizes.OrderBy(x => Convert.ToInt32(x)).ToList<string>();

Nota: esto será útil cuando todas las cadenas sean convertibles a int .....

Pranay Rana
fuente
1
esto convierte un poco la cadena en un int.
Femaref
1
"tamaños" también pueden ser no numéricos
sf.
Para "LINQ to SQL" no olvide el ToList()antes =>sizes.ToList().OrderBy(x => Convert.ToInt32(x))
A. Morel
5

Supongo que esto será mucho más bueno si tiene algo numérico en la cadena. Espero que te ayude.

PD: no estoy seguro sobre el rendimiento o los valores de cadena complicados, pero funcionó bien algo como esto:

lorem ipsum
lorem ipsum 1
lorem ipsum 2
lorem ipsum 3
...
lorem ipsum 20
lorem ipsum 21

public class SemiNumericComparer : IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        int s1r, s2r;
        var s1n = IsNumeric(s1, out s1r);
        var s2n = IsNumeric(s2, out s2r);

        if (s1n && s2n) return s1r - s2r;
        else if (s1n) return -1;
        else if (s2n) return 1;

        var num1 = Regex.Match(s1, @"\d+$");
        var num2 = Regex.Match(s2, @"\d+$");

        var onlyString1 = s1.Remove(num1.Index, num1.Length);
        var onlyString2 = s2.Remove(num2.Index, num2.Length);

        if (onlyString1 == onlyString2)
        {
            if (num1.Success && num2.Success) return Convert.ToInt32(num1.Value) - Convert.ToInt32(num2.Value);
            else if (num1.Success) return 1;
            else if (num2.Success) return -1;
        }

        return string.Compare(s1, s2, true);
    }

    public bool IsNumeric(string value, out int result)
    {
        return int.TryParse(value, out result);
    }
}
Cansın Şenalioğlu
fuente
Exactamente lo que estaba buscando. ¡Gracias!
klugerama
4

Dice que no puede convertir los números en int porque la matriz puede contener elementos que no se pueden convertir a int, pero no hay nada de malo en intentarlo:

string[] things = new string[] { "105", "101", "102", "103", "90", "paul", "bob", "lauren", "007", "90" };
Array.Sort(things, CompareThings);

foreach (var thing in things)
    Debug.WriteLine(thing);

Luego compare así:

private static int CompareThings(string x, string y)
{
    int intX, intY;
    if (int.TryParse(x, out intX) && int.TryParse(y, out intY))
        return intX.CompareTo(intY);

    return x.CompareTo(y);
}

Salida: 007, 90, 90, 101, 102, 103, 105, bob, lauren, paul

Ulf Kristiansen
fuente
Por cierto, usé Array.Sort por simplicidad, pero podrías usar la misma lógica en un IComparer y usar OrderBy.
Ulf Kristiansen
Esta solución parece más rápida que usar IComparer (mi opinión). Resultado de 15000 y creo que esto produce una segunda diferencia.
Jason Foglia
3

Esto parece una solicitud extraña y merece una solución extraña:

string[] sizes = new string[] { "105", "101", "102", "103", "90" };

foreach (var size in sizes.OrderBy(x => {
    double sum = 0;
    int position = 0;
    foreach (char c in x.ToCharArray().Reverse()) {
        sum += (c - 48) * (int)(Math.Pow(10,position));
        position++;
    }
    return sum;
}))

{
    Console.WriteLine(size);
}
Marino Šimić
fuente
Quise decir 0x30, por supuesto. Además, la matriz aún podría contener una cadena no numérica, para la cual la solución producirá resultados interesantes.
Femaref
Y tenga en cuenta que el -48 o no cambia absolutamente nada, podríamos usar directamente el valor entero del char, así que elimine ese -48 si le molesta ...
Marino Šimić
El valor char es 0x30, si lo convierte a int, seguirá siendo 0x30, que no es el número 0.
Femaref
Lo único convertido a entero es el doble que devuelve Math.Pow
Marino Šimić
Si no importa si es cero o no, el sistema decádico se encarga de eso, podría ser un Đ si quieres, lo único que importa es que los números están en orden ascendente en el juego de caracteres, y que son menos de 10
Marino Šimić
3

Este sitio analiza la clasificación alfanumérica y clasificará los números en un sentido lógico en lugar de en un sentido ASCII. También tiene en cuenta los alfas que lo rodean:

http://www.dotnetperls.com/alphanumeric-sorting

EJEMPLO:

  • C: /TestB/333.jpg
  • 11
  • C: /TestB/33.jpg
  • 1
  • C: /TestA/111.jpg
  • 111F
  • C: /TestA/11.jpg
  • 2
  • C: /TestA/1.jpg
  • 111D
  • 22
  • 111Z
  • C: /TestB/03.jpg

  • 1
  • 2
  • 11
  • 22
  • 111D
  • 111F
  • 111Z
  • C: /TestA/1.jpg
  • C: /TestA/11.jpg
  • C: /TestA/111.jpg
  • C: /TestB/03.jpg
  • C: /TestB/33.jpg
  • C: /TestB/333.jpg

El código es el siguiente:

class Program
{
    static void Main(string[] args)
    {
        var arr = new string[]
        {
           "C:/TestB/333.jpg",
           "11",
           "C:/TestB/33.jpg",
           "1",
           "C:/TestA/111.jpg",
           "111F",
           "C:/TestA/11.jpg",
           "2",
           "C:/TestA/1.jpg",
           "111D",
           "22",
           "111Z",
           "C:/TestB/03.jpg"
        };
        Array.Sort(arr, new AlphaNumericComparer());
        foreach(var e in arr) {
            Console.WriteLine(e);
        }
    }
}

public class AlphaNumericComparer : IComparer
{
    public int Compare(object x, object y)
    {
        string s1 = x as string;
        if (s1 == null)
        {
            return 0;
        }
        string s2 = y as string;
        if (s2 == null)
        {
            return 0;
        }

        int len1 = s1.Length;
        int len2 = s2.Length;
        int marker1 = 0;
        int marker2 = 0;

        // Walk through two the strings with two markers.
        while (marker1 < len1 && marker2 < len2)
        {
            char ch1 = s1[marker1];
            char ch2 = s2[marker2];

            // Some buffers we can build up characters in for each chunk.
            char[] space1 = new char[len1];
            int loc1 = 0;
            char[] space2 = new char[len2];
            int loc2 = 0;

            // Walk through all following characters that are digits or
            // characters in BOTH strings starting at the appropriate marker.
            // Collect char arrays.
            do
            {
                space1[loc1++] = ch1;
                marker1++;

                if (marker1 < len1)
                {
                    ch1 = s1[marker1];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch1) == char.IsDigit(space1[0]));

            do
            {
                space2[loc2++] = ch2;
                marker2++;

                if (marker2 < len2)
                {
                    ch2 = s2[marker2];
                }
                else
                {
                    break;
                }
            } while (char.IsDigit(ch2) == char.IsDigit(space2[0]));

            // If we have collected numbers, compare them numerically.
            // Otherwise, if we have strings, compare them alphabetically.
            string str1 = new string(space1);
            string str2 = new string(space2);

            int result;

            if (char.IsDigit(space1[0]) && char.IsDigit(space2[0]))
            {
                int thisNumericChunk = int.Parse(str1);
                int thatNumericChunk = int.Parse(str2);
                result = thisNumericChunk.CompareTo(thatNumericChunk);
            }
            else
            {
                result = str1.CompareTo(str2);
            }

            if (result != 0)
            {
                return result;
            }
        }
        return len1 - len2;
    }
}
Chad Kuehn
fuente
2

La respuesta dada por Jeff Paulsen es correcta pero Comprarerse puede simplificar mucho a esto:

public class SemiNumericComparer: IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        if (IsNumeric(s1) && IsNumeric(s2))
          return Convert.ToInt32(s1) - Convert.ToInt32(s2)

        if (IsNumeric(s1) && !IsNumeric(s2))
            return -1;

        if (!IsNumeric(s1) && IsNumeric(s2))
            return 1;

        return string.Compare(s1, s2, true);
    }

    public static bool IsNumeric(object value)
    {
        int result;
        return Int32.TryParse(value, out result);
    }
}

Esto funciona porque lo único que se comprueba para el resultado de Compareres si el resultado es mayor, menor o igual a cero. Uno puede simplemente restar los valores de otro y no tiene que manejar los valores de retorno.

Además, el IsNumericmétodo no debería tener que usar un trybloque y puede beneficiarse de TryParse.

Y para aquellos que no están seguros: este Comparer ordenará los valores de modo que los valores no numéricos siempre se agreguen al final de la lista. Si uno los quiere al principio, el segundo y tercer ifbloque deben intercambiarse.

nuez-k
fuente
Como llamar al método TryParse probablemente tenga algo de sobrecarga, primero almacenaría los valores isNumeric para s1 y s2 en valores booleanos y, en su lugar, haría la comparación con ellos. De esta forma, no se evalúan varias veces.
Optavius
1

Prueba esto :

string[] things= new string[] { "105", "101", "102", "103", "90" };

int tmpNumber;

foreach (var thing in (things.Where(xx => int.TryParse(xx, out tmpNumber)).OrderBy(xx =>     int.Parse(xx))).Concat(things.Where(xx => !int.TryParse(xx, out tmpNumber)).OrderBy(xx => xx)))
{
    Console.WriteLine(thing);
}
TMAmine
fuente
1
public class NaturalSort: IComparer<string>
{
          [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
          public static extern int StrCmpLogicalW(string x, string y);

          public int Compare(string x, string y)
          {
                 return StrCmpLogicalW(x, y);
          }
}

arr = arr.OrderBy (x => x, nuevo NaturalSort ()). ToArray ();

La razón por la que lo necesitaba era archivarme en un directorio cuyos nombres de archivo comenzaran con un número:

public static FileInfo[] GetFiles(string path)
{
  return new DirectoryInfo(path).GetFiles()
                                .OrderBy(x => x.Name, new NaturalSort())
                                .ToArray();
}
Neil M
fuente
0
Try this out..  



  string[] things = new string[] { "paul", "bob", "lauren", "007", "90", "-10" };

        List<int> num = new List<int>();
        List<string> str = new List<string>();
        for (int i = 0; i < things.Count(); i++)
        {

            int result;
            if (int.TryParse(things[i], out result))
            {
                num.Add(result);
            }
            else
            {
                str.Add(things[i]);
            }


        }

Ahora ordene las listas y vuelva a fusionarlas ...

        var strsort = from s in str
                      orderby s.Length
                      select s;

        var numsort = from n in num
                     orderby n
                     select n;

        for (int i = 0; i < things.Count(); i++)
        {

         if(i < numsort.Count())
             things[i] = numsort.ElementAt(i).ToString();
             else
             things[i] = strsort.ElementAt(i - numsort.Count());               
               }

Intenté hacer una contribución en esta interesante pregunta ...

Syeda
fuente
0

Mi solución preferida (si todas las cadenas son solo numéricas):

// Order by numerical order: (Assertion: all things are numeric strings only) 
foreach (var thing in things.OrderBy(int.Parse))
{
    Console.Writeline(thing);
}
Bug Raptor
fuente
0
public class Test
{
    public void TestMethod()
    {
        List<string> buyersList = new List<string>() { "5", "10", "1", "str", "3", "string" };
        List<string> soretedBuyersList = null;

        soretedBuyersList = new List<string>(SortedList(buyersList));
    }

    public List<string> SortedList(List<string> unsoredList)
    {
        return unsoredList.OrderBy(o => o, new SortNumericComparer()).ToList();
    }
}

   public class SortNumericComparer : IComparer<string>
{
    public int Compare(string x, string y)
    {
        int xInt = 0;
        int yInt = 0;
        int result = -1;

        if (!int.TryParse(x, out xInt))
        {
            result = 1;
        }

        if(int.TryParse(y, out yInt))
        {
            if(result == -1)
            {
                result = xInt - yInt;
            }
        }
        else if(result == 1)
        {
             result = string.Compare(x, y, true);
        }

        return result;
    }
}
Kumar
fuente
¿Puedes explicar tu código? Las respuestas de solo código pueden eliminarse.
Wai Ha Lee
La publicación de Jeff Paulsen me ayudó a implementar IComparer <string> para solucionar mi problema. .
Kumar
0

Ampliando la respuesta de Jeff Paulsen. Quería asegurarme de que no importara cuántos grupos de caracteres o números hubiera en las cadenas:

public class SemiNumericComparer : IComparer<string>
{
    public int Compare(string s1, string s2)
    {
        if (int.TryParse(s1, out var i1) && int.TryParse(s2, out var i2))
        {
            if (i1 > i2)
            {
                return 1;
            }

            if (i1 < i2)
            {
                return -1;
            }

            if (i1 == i2)
            {
                return 0;
            }
        }

        var text1 = SplitCharsAndNums(s1);
        var text2 = SplitCharsAndNums(s2);

        if (text1.Length > 1 && text2.Length > 1)
        {

            for (var i = 0; i < Math.Max(text1.Length, text2.Length); i++)
            {

                if (text1[i] != null && text2[i] != null)
                {
                    var pos = Compare(text1[i], text2[i]);
                    if (pos != 0)
                    {
                        return pos;
                    }
                }
                else
                {
                    //text1[i] is null there for the string is shorter and comes before a longer string.
                    if (text1[i] == null)
                    {
                        return -1;
                    }
                    if (text2[i] == null)
                    {
                        return 1;
                    }
                }
            }
        }

        return string.Compare(s1, s2, true);
    }

    private string[] SplitCharsAndNums(string text)
    {
        var sb = new StringBuilder();
        for (var i = 0; i < text.Length - 1; i++)
        {
            if ((!char.IsDigit(text[i]) && char.IsDigit(text[i + 1])) ||
                (char.IsDigit(text[i]) && !char.IsDigit(text[i + 1])))
            {
                sb.Append(text[i]);
                sb.Append(" ");
            }
            else
            {
                sb.Append(text[i]);
            }
        }

        sb.Append(text[text.Length - 1]);

        return sb.ToString().Split(' ');
    }
}

También tomé SplitCharsAndNums de una página SO después de enmendarla para tratar con nombres de archivos.

Tod
fuente
-1

Aunque esta es una pregunta antigua, me gustaría dar una solución:

string[] things= new string[] { "105", "101", "102", "103", "90" };

foreach (var thing in things.OrderBy(x => Int32.Parse(x) )
{
    Console.WriteLine(thing);
}

Woha bastante simple ¿verdad? :RE

Oscar Ortiz
fuente
-1
namespace X
{
    public class Utils
    {
        public class StrCmpLogicalComparer : IComparer<Projects.Sample>
        {
            [DllImport("Shlwapi.dll", CharSet = CharSet.Unicode)]
            private static extern int StrCmpLogicalW(string x, string y);


            public int Compare(Projects.Sample x, Projects.Sample y)
            {
                string[] ls1 = x.sample_name.Split("_");
                string[] ls2 = y.sample_name.Split("_");
                string s1 = ls1[0];
                string s2 = ls2[0];
                return StrCmpLogicalW(s1, s2);
            }
        }

    }
}
hamid ramezanali
fuente