Como classifico as strings em ordem alfabética enquanto considero o valor quando uma string é numérica?


100

Estou tentando classificar uma matriz de números que são strings e gostaria que eles fossem classificados numericamente.

O problema é que não consigo converter os números em inteiros .

Aqui está o código:

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

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

saída: 101, 102, 103, 105, 90

Eu gostaria: 90, 101, 102, 103, 105

EDIT: A saída não pode ser 090, 101, 102 ...

Atualizado o exemplo de código para dizer "coisas" em vez de "tamanhos". A matriz pode ser algo assim:

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

Isso significa que precisa ser classificado em ordem alfabética e por número:

007, 90, bob, lauren, paul


8
Por que você não pode convertê-los para int?
Femaref de

1
"tamanhos" pode ser outra coisa como "nome". O exemplo de código é apenas simplificado.
sf.

2
Algum dos números será negativo? Todos eles serão inteiros? Qual é o intervalo dos inteiros?
Eric Lippert

"coisas" podem ser qualquer tipo de string. Eu gostaria que a lista fosse classificada logicamente para uma pessoa que não entende de informática. Os números negativos devem vir antes da positiva. Em termos de comprimento de string, não terá mais de 100 caracteres.
sf.

5
Até onde você quer ir? Deve image10vir depois image2? Deve Januaryvir antes February?
svick de

Respostas:


104

Passe um comparador personalizado para OrderBy. Enumerable.OrderBy permitirá que você especifique qualquer comparador de sua preferência.

Esta é uma maneira de fazer isso:

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

1
Para a entrada fornecida, produz o mesmo resultado que a resposta de Recursive, que envolve PadLeft (). Estou assumindo que sua entrada é realmente mais complexa do que mostra este exemplo, caso em que um comparador personalizado é o caminho a percorrer.
Jeff Paulsen

Felicidades. Esta solução funciona e parece ser uma maneira fácil de ler e implementar. +1 por me mostrar que você pode usar IComparer em OrderBy :)
sf.

17
O IsNumericmétodo é ruim, uma codificação conduzida por Exceções é sempre ruim. Use em seu int.TryParselugar. Teste seu código com uma lista grande e isso levará uma eternidade.
Nean Der Thal,

Se for útil, adicionei uma extensão a esta versão aqui que adiciona suporte para classificação por palavras. Para minhas necessidades, dividir em espaços era suficiente e eu não precisava me preocupar com palavras de uso misto (por exemplo, test12 vs test3),
matt.bungard

@NeanDerThal Tenho certeza de que só é lento / incorreto o tratamento de muitas exceções em um loop, se você estiver depurando ou acessando o objeto Exception.
Kelly Elton

90

Basta preencher com zeros do mesmo comprimento:

int maxlen = sizes.Max(x => x.Length);
var result = sizes.OrderBy(x => x.PadLeft(maxlen, '0'));

+1 para solução simples, detalhamento seria (já feito na edição, bom)
Marino Šimić

Boa ideia, mas o próximo problema é que preciso exibir esses valores para que "90" seja um "90", não "090"
sf.

6
@sf: Experimente, você pode gostar do resultado. Lembre-se de que a chave de pedido não é o item que está sendo pedido. Se eu disser para pedir uma lista de clientes pelo sobrenome, recebo uma lista de clientes, não uma lista de sobrenomes. Se você disser para ordenar uma lista de strings por uma string transformada, o resultado será a lista ordenada de strings originais, não strings transformadas.
Eric Lippert

Eu tive que adicionar "tamanhos = tamanhos.OrderBy (...)" para fazer este trabalho. Isso é normal ou a resposta deve ser editada?
gorgabal

1
@gorgabal: Em geral, reatribuir para sizestambém não funcionaria, porque o resultado é um tipo diferente. A resposta é meio abreviada, pois a segunda linha mostra o resultado como uma expressão, mas cabe ao leitor fazer algo com ele. Eu adicionei outra atribuição de variável para deixar isso mais claro.
recursivo

74

E, que tal isso ...

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

hehe, eu realmente gosto deste - muito inteligente. Desculpe se eu não forneci o conjunto completo de dados iniciais
sf.

3
Isso é como a opção de pad acima, apenas muito melhor IMO.
dudeNumber4 01 de

3
var size = size.OrderBy (x => x.Length) .ThenBy (x => x);
Phillip Davis

1
Mas isso vai misturar cordas alfabéticos como este: "b", "ab", "101", "103", "bob", "abcd".
Andrew,

67

O valor é uma string

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

Trabalho


2
Esta resposta é minha favorita.
LacOniC

2
Obrigado, acabei de descobrir que existe um método "ThenBy".
ganchito55

Isso funciona muito bem para o meu caso de uso, em que a entrada está no formato novostring[] { "Object 1", "Object 9", "Object 14" }
thelem

2
Esta é a melhor resposta. Funciona e é um bom aprendizado. Obrigado !!
Julgamento Ordinário

1
Mas isso vai misturar cordas alfabéticos como este: "b", "ab", "101", "103", "bob", "abcd".
Andrew,

13

Há uma função nativa no Windows StrCmpLogicalWque compara os números das strings como números em vez de letras. É fácil fazer um comparador que chama aquela função e a usa para suas comparações.

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

Ele funciona até mesmo em strings com texto e números. Aqui está um programa de exemplo que irámostrar a diferença entre a classificação padrão e a StrCmpLogicalWclassificação

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

quais saídas

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

Gostaria que fosse mais fácil usar as bibliotecas do sistema em C #
Kyle Delaney,

Isso teria sido perfeito, mas infelizmente não aceita números negativos. -1 0 10 2é classificado como0 -1 2 10
nphx

5

tente isso

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

Observação: isso será útil quando todas as strings forem conversíveis em int .....


1
isso meio que converte a string em um int.
Femaref de

1
"tamanhos" também podem ser não numéricos
sf.

Para "LINQ to SQL", não se esqueça do ToList()anterior =>sizes.ToList().OrderBy(x => Convert.ToInt32(x))
A. Morel

5

Eu acho que isso será muito mais bom se tiver algum valor numérico na string. Espero que ajude.

PS: Não tenho certeza sobre desempenho ou valores de string complicados, mas funcionou bem em algo assim:

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

Exatamente o que eu estava procurando. Obrigado!
Klugerama

4

Você diz que não pode converter os números em int porque a matriz pode conter elementos que não podem ser convertidos em int, mas não há mal em tentar:

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

Em seguida, compare assim:

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

Resultado: 007, 90, 90, 101, 102, 103, 105, bob, Lauren, Paul


A propósito, usei Array.Sort para simplificar, mas você poderia usar a mesma lógica em um IComparer e usar OrderBy.
Ulf Kristiansen

Esta solução parece mais rápida do que usar IComparer (minha opinião). 15000 resultado e eu sinto que isso rende cerca de uma segunda diferença.
Jason Foglia

3

Este parece um pedido estranho e merece uma solução estranha:

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

Eu quis dizer 0x30, é claro. Além disso, a matriz ainda pode conter uma string não numérica, para a qual a solução produzirá resultados interessantes.
Femaref de

E note que o -48 ou não muda absolutamente nada, poderíamos usar diretamente o valor inteiro do char, então remova esse -48 se incomodar você ...
Marino Šimić

O valor char é 0x30, se você converter para int, ainda será 0x30, que não é o número 0.
Femaref

A única coisa convertida para inteiro é o dobro que é retornado de Math.Pow
Marino Šimić

femaref não importa se é zero ou não, o sistema decádico cuida disso, pode ser um Đ se quiser, a única coisa que importa é que os números estão em ordem crescente no conjunto de caracteres, e que são menos de 10
Marino Šimić

3

Este site discute a classificação alfanumérica e classificará os números em um sentido lógico em vez de um sentido ASCII. Ele também leva em consideração os alfas ao seu redor:

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

EXEMPLO:

  • 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

O código é o seguinte:

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

2

A resposta dada por Jeff Paulsen está correta, mas Comprarerpode ser muito simplificada para isto:

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

Isso funciona porque a única coisa que é verificada para o resultado de Compareré se o resultado é maior, menor ou igual a zero. Pode-se simplesmente subtrair os valores de outro e não precisa lidar com os valores de retorno.

Além disso, o IsNumericmétodo não deve ter que usar um try-block e pode se beneficiar dele TryParse.

E para aqueles que não têm certeza: Este Comparador classificará os valores de forma que os valores não numéricos sejam sempre anexados ao final da lista. Se alguém os quiser no início, o segundo e o terceiro ifbloco devem ser trocados.


Como chamar o método TryParse provavelmente tem alguma sobrecarga, eu armazenaria os valores isNumeric para s1 e s2 em valores booleanos primeiro e faria a comparação nesses valores. Dessa forma, eles não são avaliados várias vezes.
Optavius

1

Experimente isto:

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

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, novo NaturalSort ()). ToArray ();

O motivo pelo qual eu precisava era para ser arquivado em um diretório cujos nomes de arquivo começavam com um número:

public static FileInfo[] GetFiles(string path)
{
  return new DirectoryInfo(path).GetFiles()
                                .OrderBy(x => x.Name, new NaturalSort())
                                .ToArray();
}

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


        }

Agora classifique as listas e mescle-as de volta ...

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

Só tentei dar uma contribuição nesta questão interessante ...


0

Minha solução preferida (se todas as strings forem apenas numéricas):

// Order by numerical order: (Assertion: all things are numeric strings only) 
foreach (var thing in things.OrderBy(int.Parse))
{
    Console.Writeline(thing);
}

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

Você pode explicar seu código? Respostas apenas em código podem ser excluídas.
Wai Ha Lee

A postagem de Jeff Paulsen me ajudou a implementar IComparer <string> para corrigir meu problema de ferida. .
Kumar,

0

Expandindo a resposta de Jeff Paulsen. Eu queria ter certeza de que não importava quantos grupos de números ou caracteres existissem nas strings:

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

Eu também peguei SplitCharsAndNums de uma página do SO depois de alterá- la para lidar com nomes de arquivos.


-1

Embora esta seja uma questão antiga, gostaria de dar uma solução:

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

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

Woha bem simples, certo? : D


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

    }
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.