Come faccio a ordinare le stringhe in ordine alfabetico tenendo conto del valore quando una stringa è numerica?


100

Sto cercando di ordinare un array di numeri che sono stringhe e vorrei che ordinassero numericamente.

Il problema è che non riesco a convertire i numeri in int .

Ecco il codice:

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

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

uscita: 101, 102, 103, 105, 90

Vorrei: 90, 101, 102, 103, 105

EDIT: l'uscita non può essere 090, 101, 102 ...

Aggiornato il codice di esempio per dire "cose" invece di "dimensioni". L'array può essere qualcosa del genere:

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

Ciò significa che deve essere ordinato alfabeticamente e per numero:

007, 90, bob, lauren, paul


8
Perché non puoi convertirli in int?
Femaref

1
"dimensioni" può essere qualcos'altro come "nome". L'esempio di codice è semplicemente semplificato.
sf.

2
Qualcuno dei numeri sarà negativo? Saranno tutti numeri interi? Qual è l'intervallo degli interi?
Eric Lippert

"cose" possono essere qualsiasi tipo di stringa. Vorrei che l'elenco fosse ordinato logicamente a una persona non esperta di computer. I numeri negativi dovrebbero essere prima di postivi. In termini di lunghezza della stringa, non sarà più di 100 caratteri.
sf.

5
Quanto lontano vuoi arrivare? Dovrebbe image10venire dopo image2? Dovrebbe Januaryvenire prima February?
svick

Risposte:


104

Passa un comparatore personalizzato a OrderBy. Enumerable.OrderBy ti consente di specificare qualsiasi comparatore che ti piace.

Questo è un modo per farlo:

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
Per l'input fornito, questo produce lo stesso risultato della risposta di Recursive, che coinvolge PadLeft (). Suppongo che il tuo input sia in realtà più complesso di quanto mostrato in questo esempio, nel qual caso un comparatore personalizzato è la strada da percorrere.
Jeff Paulsen

Saluti. Questa soluzione funziona e sembra un modo semplice da leggere e da implementare. +1 per avermi mostrato che puoi usare IComparer su OrderBy :)
sf.

17
Il IsNumericmetodo è pessimo, una codifica guidata dalle eccezioni è sempre pessima. Usa int.TryParseinvece. Prova il tuo codice con un lungo elenco e ci vorrà un'eternità.
Nean Der Thal

Se è utile, ho aggiunto un'estensione a questa versione qui , che aggiunge il supporto per l'ordinamento con le parole. Per le mie esigenze, la suddivisione in spazi era sufficiente e non avevo bisogno di preoccuparmi delle parole di uso misto (ad esempio test12 vs test3),
matt.bungard

@NeanDerThal Sono abbastanza sicuro che sia solo una gestione lenta / cattiva di molte eccezioni in un ciclo, se stai eseguendo il debug o stai accedendo all'oggetto Exception.
Kelly Elton

90

Basta riempire con zeri della stessa lunghezza:

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

+1 per soluzione semplice, il pelo nell'uovo sarebbe (già fatto in modifica, bello)
Marino Šimić

Bella idea, ma il problema successivo è che devo visualizzare questi valori in modo che "90" debba essere "90", non "090"
sf.

6
@sf: provalo, il risultato potrebbe piacerti. Ricorda, la chiave dell'ordine non è l'oggetto ordinato. Se ho detto di ordinare un elenco di clienti per cognome, ottengo un elenco di clienti, non un elenco di cognomi. Se dici di ordinare un elenco di stringhe in base a una stringa trasformata, il risultato è l'elenco ordinato di stringhe originali, non stringhe trasformate.
Eric Lippert

Ho dovuto aggiungere "size =izes.OrderBy (...)" per farlo funzionare. È normale o la risposta dovrebbe essere modificata?
gorgabal il

1
@gorgabal: In generale, anche la riassegnazione a sizesnon funzionerebbe, perché il risultato è di un tipo diverso. La risposta è un po 'breve, in quanto la seconda riga mostra il risultato come un'espressione, ma spetta al lettore fare qualcosa con esso. Ho aggiunto un'altra assegnazione di variabile per renderlo più chiaro.
ricorsivo

74

E che ne dici di questo ...

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, mi piace molto questo - molto intelligente. Scusa se non ho fornito il set completo di dati iniziali
sf.

3
Questo è proprio come l'opzione pad sopra solo IMO molto migliore.
dudeNumber4

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

1
Ma questo mix stringhe alfabetiche come questo: "b", "ab", "101", "103", "bob", "abcd".
Andrew

67

Il valore è una stringa

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

Lavori


2
Questa risposta è la mia preferita.
LacOniC

2
Grazie ho appena scoperto che esce un metodo "ThenBy".
ganchito55

Questa grande opera per il mio caso d'uso, in cui l'ingresso è nel nuovo formatostring[] { "Object 1", "Object 9", "Object 14" }
Thelem

2
Questa è la migliore risposta. Funziona ed è un buon apprendimento. Grazie !!
luglioOrdinario

1
Ma questo mix stringhe alfabetiche come questo: "b", "ab", "101", "103", "bob", "abcd".
Andrew

13

C'è una funzione nativa in Windows StrCmpLogicalWche confronterà in stringhe i numeri come numeri invece che lettere. È facile creare un comparatore che richiami quella funzione e la utilizzi per i suoi confronti.

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

Funziona anche su stringhe che contengono sia testo che numeri. Ecco un programma di esempio che mostrerà la differenza tra l'ordinamento predefinito e l' StrCmpLogicalWordinamento

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

quali uscite

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

Vorrei che fosse più facile usare le librerie di sistema in C #
Kyle Delaney

Sarebbe stato perfetto, ma sfortunatamente non gestisce numeri negativi. -1 0 10 2è ordinato come0 -1 2 10
nphx

5

prova questo

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

Nota: questo sarà utile quando tutti sono convertibili in stringa in int .....


1
questo tipo converte la stringa in un int.
Femaref

1
le "dimensioni" possono anche essere non numeriche
sf.

Per "LINQ to SQL" non dimenticare il ToList()precedente =>sizes.ToList().OrderBy(x => Convert.ToInt32(x))
A. Morel

5

Immagino che questo sarà molto più buono se ha un valore numerico nella stringa. Spero che possa aiutare.

PS: non sono sicuro delle prestazioni o dei valori di stringa complicati, ma ha funzionato bene qualcosa del genere:

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

Esattamente quello che stavo cercando. Grazie!
klugerama

4

Dici che non puoi convertire i numeri in int perché l'array può contenere elementi che non possono essere convertiti in int, ma non c'è nulla di male nel provare:

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

Quindi confronta in questo modo:

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

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


A proposito, ho usato Array.Sort per semplicità, ma potresti usare la stessa logica in un IComparer e usare OrderBy.
Ulf Kristiansen

Questa soluzione sembra più veloce quindi utilizzando IComparer (la mia opinione). 15000 risultati e sento che questo produce circa una seconda differenza.
Jason Foglia

3

Questa sembra una strana richiesta e merita una strana soluzione:

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

Intendevo 0x30 ovviamente. Inoltre, l'array potrebbe ancora contenere una stringa non numerica, per la quale la soluzione produrrà risultati interessanti.
Femaref

E nota che -48 o non cambia assolutamente nulla, potremmo usare direttamente il valore intero del carattere, quindi rimuovi quel -48 se ti dà fastidio ...
Marino Šimić

Il valore del carattere è 0x30, se lo converti in int, sarà ancora 0x30, che non è il numero 0.
Femaref

L'unica cosa convertita in intero è il doppio restituito da Math.Pow
Marino Šimić

femaref non importa se è zero o no, il sistema decadico si prende cura di questo, potrebbe essere un the se vuoi, l'unica cosa che importa che i numeri siano in ordine crescente nel set di caratteri, e che siano meno di 10
Marino Šimić

3

Questo sito discute l'ordinamento alfanumerico e ordinerà i numeri in senso logico invece che ASCII. Tiene conto anche degli alfa che lo circondano:

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

ESEMPIO:

  • 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

Il codice è il seguente:

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

La risposta data da Jeff Paulsen è corretta ma Comprarerpuò essere molto semplificata in questo modo:

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

Questo funziona perché l'unica cosa che viene controllata per il risultato di Comparer è se il risultato è maggiore, minore o uguale a zero. Si può semplicemente sottrarre i valori da un altro e non è necessario gestire i valori di ritorno.

Inoltre il IsNumericmetodo non dovrebbe utilizzare un tryblocco e può trarne vantaggio TryParse.

E per coloro che non sono sicuri: questo Comparer ordinerà i valori in modo che i valori non numerici vengano sempre aggiunti alla fine dell'elenco. Se si vogliono all'inizio, il secondo e il terzo ifblocco devono essere scambiati.


Poiché la chiamata al metodo TryParse probabilmente ha un sovraccarico, memorizzerei prima i valori isNumeric per s1 e s2 in valori booleani e farei invece il confronto su quelli. In questo modo non vengono valutati più volte.
Optavius

1

Prova questo :

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

Il motivo per cui avevo bisogno era di essere archiviato in una directory i cui nomi di file iniziavano con un numero:

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


        }

Ora ordina gli elenchi e uniscili di nuovo ...

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

Ho solo cercato di dare un contributo a questa interessante domanda ...


0

La mia soluzione preferita (se tutte le stringhe sono solo numeriche):

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

Puoi spiegare il tuo codice? Le risposte di solo codice possono essere eliminate.
Wai Ha Lee

Il post di Jeff Paulsen mi ha aiutato a implementare IComparer <string> per risolvere il mio problema di smistamento. .
kumar

0

Espandendo la risposta di Jeff Paulsen. Volevo assicurarmi che non importasse quanti numeri o gruppi di caratteri erano nelle stringhe:

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

Ho anche preso SplitCharsAndNums da una pagina SO dopo averlo modificato per gestire i nomi dei file.


-1

Anche se questa è una vecchia domanda, vorrei dare una soluzione:

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

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

Woha abbastanza semplice, vero? : 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);
            }
        }

    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.