2011-08-26 4 views
10

Ho bisogno di ordinare i nomi dei file come segue: 1.Accedere, 2.log, 10.logQual è il modo più breve in .NET per ordinare le stringhe che iniziano con 1, 10 e 2 e rispettare l'ordinamento dei numeri?

Ma quando uso OrderBy (fn => fn) li fascicolerà come: 1.Accedere, 10. log, 2.log

Ovviamente so che questo potrebbe essere fatto scrivendo un altro comparatore, ma c'è un modo più semplice per passare dall'ordine lessicografico all'ordinamento naturale?

Modifica: l'obiettivo è ottenere lo stesso ordinamento di quando si seleziona "ordina per nome" in Esplora risorse.

risposta

5

È possibile utilizzare la funzione Win32 CompareStringEx. Su Windows 7 supporta l'ordinamento di cui hai bisogno. Avrete uso P/Invoke:

static readonly Int32 NORM_IGNORECASE = 0x00000001; 
static readonly Int32 NORM_IGNORENONSPACE = 0x00000002; 
static readonly Int32 NORM_IGNORESYMBOLS = 0x00000004; 
static readonly Int32 LINGUISTIC_IGNORECASE = 0x00000010; 
static readonly Int32 LINGUISTIC_IGNOREDIACRITIC = 0x00000020; 
static readonly Int32 NORM_IGNOREKANATYPE = 0x00010000; 
static readonly Int32 NORM_IGNOREWIDTH = 0x00020000; 
static readonly Int32 NORM_LINGUISTIC_CASING = 0x08000000; 
static readonly Int32 SORT_STRINGSORT = 0x00001000; 
static readonly Int32 SORT_DIGITSASNUMBERS = 0x00000008; 

static readonly String LOCALE_NAME_USER_DEFAULT = null; 
static readonly String LOCALE_NAME_INVARIANT = String.Empty; 
static readonly String LOCALE_NAME_SYSTEM_DEFAULT = "!sys-default-locale"; 

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] 
static extern Int32 CompareStringEx(
    String localeName, 
    Int32 flags, 
    String str1, 
    Int32 count1, 
    String str2, 
    Int32 count2, 
    IntPtr versionInformation, 
    IntPtr reserved, 
    Int32 param 
); 

È possibile quindi creare un IComparer che utilizza il flag SORT_DIGITSASNUMBERS:

class LexicographicalComparer : IComparer<String> { 

    readonly String locale; 

    public LexicographicalComparer() : this(CultureInfo.CurrentCulture) { } 

    public LexicographicalComparer(CultureInfo cultureInfo) { 
    if (cultureInfo.IsNeutralCulture) 
     this.locale = LOCALE_NAME_INVARIANT; 
    else 
     this.locale = cultureInfo.Name; 
    } 

    public Int32 Compare(String x, String y) { 
    // CompareStringEx return 1, 2, or 3. Subtract 2 to get the return value. 
    return CompareStringEx( 
     this.locale, 
     SORT_DIGITSASNUMBERS, // Add other flags if required. 
     x, 
     x.Length, 
     y, 
     y.Length, 
     IntPtr.Zero, 
     IntPtr.Zero, 
     0) - 2; 
    } 

} 

è quindi possibile utilizzare il IComparer in vari ordinamento API:

var names = new [] { "2.log", "10.log", "1.log" }; 
var sortedNames = names.OrderBy(s => s, new LexicographicalComparer()); 

È inoltre possibile utilizzare StrCmpLogicalW, che è la funzione utilizzata da Esplora risorse. È disponibile da Windows XP:

[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] 
static extern Int32 StrCmpLogical(String x, String y); 

class LexicographicalComparer : IComparer<String> { 

    public Int32 Compare(String x, String y) { 
    return StrCmpLogical(x, y); 
    } 

} 

Più semplice, ma si ha meno controllo sul confronto.

0

Sarebbe più facile se fosse un ordine lessicografico.

Il confronto delle stringhe è sempre lettera per lettera.

Come si vuole gestire questo senza guardare l'intero numero?

No, un unico comparatore è l'unica soluzione.

2

Si potrebbe semplicemente rimuovere tutti i caratteri non numerici, analizzare a int e poi scegli:

Regex r = new Regex(@"[^\d]"); 
OrderBy(fn => int.Parse(r.Replace(fn, ""))); 
+0

Probabilmente non molto veloce, ma la soluzione più succinta che non richiede la modifica dei nomi dei file in altre cose. –

+2

Questo è debole. Mette 1.10 dopo il 10.1 –

+0

@Chris ordina 100000 elementi in 0,2 secondi. Spero che non abbia comunque tanti file di log –

0

no io non la penso così - credo che si deve scrivere da soli il tempo che i vostri dati sono solo una stringa. se fate i vostri dati in qualcosa di simile

struct LogDescription 
{ 
    public int LogBase { get; set; } 
    public override ToString() 
    { return string.Format("{0}.log", LogBase); } 
} 

è possibile ordinare tramite logbase-Field

2

Il modo più semplice (non necessariamente più veloce/ottimale) sarebbe IMHO a tutti loro sinistra-pad in una certa lunghezza massima predefinita con zeri. Cioè

var data = new[] { "1.log", "10.log", "2.log" }; 
data.OrderBy(x => x.PadLeft(10, '0')).Dump(); 
0

Si può fare qualcosa di simile quando si può assicurare il formato dei vostri nomi sono NUMBER.VALUE:

var q = strings.Select(s => s.Split(new[] {'.'}, 2)) 
    .Select(s => new 
         { 
          Number = Convert.ToInt32(s[0]), 
          Name = s[1] 
         }) 
    .OrderBy(s => s.Number) 
    .Select(s => string.Format("{0}.{1}", s.Number, s.Name)); 
4

Se i nomi di file sempre consistono solo in cifre, è possibile utilizzare Path.GetFileNameWithoutExtension() scartare l'estensione del file e Convert.ToInt32() (o simile) per convertire i nomi di file di numeri interi a scopo di confronto:

var ordered = yourFileNames.OrderBy(
    fn => Convert.ToInt32(Path.GetFileNameWithoutExtension(fn))); 

Nel caso generale, o se siete l Per ottenere un modo più "standard" per farlo, è possibile p/invocare StrCmpLogicalW(), che Explorer utilizza per ordinare i nomi dei file nelle sue viste. Tuttavia, facendo ciò ti costringerà ad implementare un IComparer<string> se si desidera utilizzare OrderBy().

+0

Grazie, questo è ciò che avevo inizialmente scritto; comunque spero in un modo più standard per fare questo confronto, se c'è ... –

+0

Pensavo anche a questo modo. Ma se c'è anche un singolo file come 'config.txt' nella cartella di registro, questo si bloccherebbe –

+1

Questo, tuttavia, esploderà molto bene se ci sono caratteri finali nel nome del file (ad esempio 4service.log) – sehe