2015-10-13 6 views
10

C'è un modo per rendere le funzioni C# TryParse() un po 'più ... severe?decimal.TryParse accetta volentieri le stringhe numeriche formattate male

In questo momento, se si passa in una stringa contenente numeri, i corretti decimali & mille caratteri di separazione, spesso sembra solo ad accettarle, anche se il formato non ha senso, ad esempio: 123''345'678

I Sto cercando un modo per rendere TryParsesenza successo se il numero non è nel formato giusto.

Quindi, sto con sede a Zurigo, e se faccio questo:

decimal exampleNumber = 1234567.89m; 
Trace.WriteLine(string.Format("Value {0} gets formatted as: \"{1:N}\"", exampleNumber, exampleNumber)); 

... poi, con le mie impostazioni internazionali, ottengo questo ...

Value 1234567.89 gets formatted as: "1'234'567.89" 

Così puoi vedere che, per la mia regione, il carattere decimale è un punto e il mille separatore è un apostrofo.

Ora, creiamo una semplice funzione per verificare se un string può essere analizzato in un decimal:

private void ParseTest(string str) 
{ 
    decimal val = 0; 
    if (decimal.TryParse(str, out val)) 
     Trace.WriteLine(string.Format("Parsed \"{0}\" as {1}", str, val)); 
    else 
     Trace.WriteLine(string.Format("Couldn't parse: \"{0}\"", str)); 
} 

Va bene, chiamiamo questa funzione con alcune stringhe.

Quale delle seguenti stringhe sarebbe si pensa che venga analizzato correttamente da questa funzione?

Qui di seguito sono i risultati che ho ottenuto:

ParseTest("123345.67");   // 1. Parsed "123345.67" as 123345.67 
ParseTest("123'345.67");  // 2. Parsed "123'345.67" as 123345.67 
ParseTest("123'345'6.78");  // 3. Parsed "123'345'6.78" as 1233456.78 
ParseTest("1''23'345'678");  // 4. Parsed "1''23'345'678" as 123345678 
ParseTest("'1''23'345'678"); // 5. Couldn't parse: "'1''23'345'678" 
ParseTest("123''345'678");  // 6. Parsed "123''345'678" as 123345678 
ParseTest("123'4'5'6.7.89"); // 7. Couldn't parse: "123'4'5'6.7.89" 
ParseTest("'12'3'45'678");  // 8. Couldn't parse: "'12'3'45'678" 

penso che si può vedere il mio punto.

Per me, solo le prime due stringhe avrebbero dovuto analizzare correttamente. Gli altri dovrebbero aver fallito tutti, dato che non hanno 3 cifre dopo mille separatori, o hanno due apostrofi insieme.

Anche se cambio lo ParseTest in modo un po 'più specifico, i risultati sono esattamente gli stessi. (Per esempio, accetta felicemente "123''345'678" come un decimale valido.)

private void ParseTest(string str) 
{ 
    decimal val = 0; 
    var styles = (NumberStyles.AllowDecimalPoint | NumberStyles.AllowThousands); 

    if (decimal.TryParse(str, styles, CultureInfo.CurrentCulture, out val)) 
     Trace.WriteLine(string.Format("Parsed \"{0}\" as {1}", str, val)); 
    else 
     Trace.WriteLine(string.Format("Couldn't parse: \"{0}\"", str)); 
} 

Quindi, c'è un modo semplice per non consentono stringhe formattate male per essere accettato da TryParse?

Aggiornamento

Grazie per tutti i suggerimenti.

Forse dovrei chiarire: quello che sto cercando è che le prime due stringhe siano valide, ma la terza deve essere rifiutata.

ParseTest("123345.67"); 
ParseTest("123'456.67"); 
ParseTest("12'345'6.7"); 

Sicuramente ci deve essere un modo per utilizzare "NumberStyles.AllowThousands" in modo che possa consentire opzionalmente mille-separatori, ma assicurarsi che il formato numerico fa ha senso?

In questo momento, se uso questo:

if (decimal.TryParse(str, styles, CultureInfo.CurrentCulture, out val)) 

ottengo questi risultati:

Parsed "123345.67" as 123345.67 
Parsed "123'456.67" as 123456.67 
Parsed "12'345'6.7" as 123456.7 

E se uso questo:

if (decimal.TryParse(str, styles, CultureInfo.InvariantCulture, out val)) 

ottengo questi risultati:

Parsed "123345.67" as 123345.67 
Couldn't parse: "123'456.67" 
Couldn't parse: "12'345'6.7" 

Questo è il mio problema ... indipendentemente dalle impostazioni di CultureInfo, la terza stringa deve essere rifiutata e le prime due accettate.

+2

Devi accettare * valido * migliaia di separatori? In caso contrario, non utilizzare AllowThousands ... –

+0

Sembra che sia necessaria una convalida di espressioni regolari separata. –

+0

Onestamente, sono sorpreso che adiacente a migliaia i separatori vengano analizzati con successo _even_ quando si assegna la proprietà corretta nell'esempio '123''345'678'. Ma forse hai solo bisogno di assegnare la proprietà 'NumberGroupSizes' come' {3, 3, 0} 'in questo caso. Non lo so. –

risposta

1

È perché l'analisi salta semplicemente la stringa NumberFormatInfo.NumberGroupSeparator e ignora completamente la proprietà NumberFormatInfo.NumberGroupSizes. Tuttavia, è possibile implementare un tale convalida:

static bool ValidateNumberGroups(string value, CultureInfo culture) 
{ 
    string[] parts = value.Split(new string[] { culture.NumberFormat.NumberGroupSeparator }, StringSplitOptions.None); 
    foreach (string part in parts) 
    { 
     int length = part.Length; 
     if (culture.NumberFormat.NumberGroupSizes.Contains(length) == false) 
     { 
      return false; 
     } 
    } 

    return true; 
} 

Non è ancora completamente perfetto, come MSDN says:

Il primo elemento della matrice definisce il numero di elementi in meno significativo gruppo di cifre immediatamente a sinistra di NumberDecimalSeparator. Ogni elemento successivo fa riferimento al successivo gruppo significativo di cifre a sinistra del gruppo precedente. Se l'ultimo elemento dell'array non è 0, le cifre rimanenti vengono raggruppate in base all'ultimo elemento dell'array. Se l'ultimo elemento è 0, le cifre rimanenti non sono raggruppate.

Ad esempio, se la matrice contiene {3, 4, 5}, le cifre sono raggruppate in modo simile a "55,55555,55555,55555,4444,333.00". Se la matrice contiene {3, 4, 0}, le cifre sono raggruppate in modo simile a "55555555555555555,4444,333.00".

Ma ora puoi vedere il punto.

2

Il modo più semplice per stabilire se è formattato correttamente in base alla cultura corrente è quello di confrontare il numero risultante dopo la formattazione con la stringa originale.

//input = "123,456.56" -- true 
//input = "123,4,56.56" -- false 
//input = "123456.56" -- true 
//input = "123,,456.56" -- false 
string input = "123456.56"; 
decimal value; 

if(!decimal.TryParse(input, out value)) 
{ 
    return false; 
} 

return (value.ToString("N") == input || value.ToString() == input); 

Questo avrà successo per gli ingressi che omettono completamente separatori delle migliaia e gli input che specificano corretti separatori delle migliaia.

Se è necessario accettare un intervallo di cifre decimali, è necessario acquisire il numero di caratteri dopo il separatore decimale e aggiungerlo alla stringa di formato "N".

+1

Bello. Esattamente quello che stavo cercando. Mi scuso per aver sprecato il tempo di tutti su questo tema, ma ho pensato che, dopo tutti gli anni in cui TryParse è stato utilizzato, ci deve essere un modo per riconoscere stringhe formattate male. Non posso essere la prima persona in cerca di una funzione di analisi del "senso comune" ...! –

+0

Ho pensato lungo la stessa linea. Ma per quanto riguarda le stringhe come "" 12'345.666 "' o '" 12'345.6 "'. Si sentono abbastanza innocenti, ma poiché il numero di decimali dopo il punto '.' non è esattamente due, vengono respinti dalla soluzione. Questo problema può essere risolto con '.ToString (" #, 0. ##############################) 'o cose brutte simili . –

+0

Ho già incluso una soluzione per la lunghezza variata dopo il punto decimale alla fine della risposta. N0, N1, N8 ecc sarebbero stringhe di formato valide – ndonohoe

1

Mettendo insieme tutti i suggerimenti utili qui, ecco cosa ho finito per usare.

Non è perfetto, ma, per la mia app aziendale, rifiuta almeno le stringhe numeriche che "non sembrano giuste".

Prima vi presento il mio codice, ecco le differenze tra ciò che la mia funzione TryParseExact accetterà, e ciò che il regolare decimal.TryParse avrebbe accettato:

enter image description here

Ed ecco il mio codice.

Sono sicuro che c'è un modo più efficiente di fare un po 'di questo, utilizzando regex o qualcosa del genere, ma questo è sufficiente per le mie esigenze, e spero che aiuta altri sviluppatori:

public static bool TryParseExact(string str, out decimal result) 
    { 
     // The regular decimal.TryParse() is a bit rubbish. It'll happily accept strings which don't make sense, such as: 
     //  123'345'6.78 
     //  1''23'345'678 
     //  123''345'678 
     // 
     // This function does the same as TryParse(), but checks whether the number "makes sense", ie: 
     //  - has exactly zero or one "decimal point" characters 
     //  - if the string has thousand-separators, then are there exactly three digits inbetween them 
     // 
     // Assumptions: if we're using thousand-separators, then there'll be just one "NumberGroupSizes" value. 
     // 
     // Returns True if this is a valid number 
     //   False if this isn't a valid number 
     // 
     result = 0; 

     if (str == null || string.IsNullOrWhiteSpace(str)) 
      return false; 

     // First, let's see if TryParse itself falls over, trying to parse the string. 
     decimal val = 0; 
     if (!decimal.TryParse(str, out val)) 
     { 
      // If the numeric string contains any letters, foreign characters, etc, the function will abort here. 
      return false; 
     } 

     // Note: we'll ONLY return TryParse's result *if* the rest of the validation succeeds. 

     CultureInfo culture = CultureInfo.CurrentCulture; 
     int[] expectedDigitLengths = culture.NumberFormat.NumberGroupSizes;   // Usually a 1-element array: { 3 } 
     string decimalPoint = culture.NumberFormat.NumberDecimalSeparator;   // Usually full-stop, but perhaps a comma in France. 
     string thousands = culture.NumberFormat.NumberGroupSeparator;    // Usually a comma, but can be apostrophe in European locations. 

     int numberOfDecimalPoints = CountOccurrences(str, decimalPoint); 
     if (numberOfDecimalPoints != 0 && numberOfDecimalPoints != 1) 
     { 
      // You're only allowed either ONE or ZERO decimal point characters. No more! 
      return false; 
     } 

     int numberOfThousandDelimiters = CountOccurrences(str, thousands); 
     if (numberOfThousandDelimiters == 0) 
     { 
      result = val; 
      return true; 
     } 

     // Okay, so this numeric-string DOES contain 1 or more thousand-seperator characters. 
     // Let's do some checks on the integer part of this numeric string (eg "12,345,67.890" -> "12,345,67") 
     if (numberOfDecimalPoints == 1) 
     { 
      int inx = str.IndexOf(decimalPoint); 
      str = str.Substring(0, inx); 
     } 

     // Split up our number-string into sections: "12,345,67" -> [ "12", "345", "67" ] 
     string[] parts = str.Split(new string[] { thousands }, StringSplitOptions.None); 

     if (parts.Length < 2) 
     { 
      // If we're using thousand-separators, then we must have at least two parts (eg "1,234" contains two parts: "1" and "234") 
      return false; 
     } 

     // Note: the first section is allowed to be upto 3-chars long (eg for "12,345,678", the "12" is perfectly valid) 
     if (parts[0].Length == 0 || parts[0].Length > expectedDigitLengths[0]) 
     { 
      // This should catch errors like: 
      //  ",234" 
      //  "1234,567" 
      //  "12345678,901" 
      return false; 
     } 

     // ... all subsequent sections MUST be 3-characters in length 
     foreach (string oneSection in parts.Skip(1)) 
     { 
      if (oneSection.Length != expectedDigitLengths[0]) 
       return false; 
     } 

     result = val; 
     return true; 
    } 

    public static int CountOccurrences(string str, string chr) 
    { 
     // How many times does a particular string appear in a string ? 
     // 
     int count = str.Length - str.Replace(chr, "").Length; 
     return count; 
    } 

Btw, I creato l'immagine precedente tabella in Excel, e ho notato che in realtà è difficile da incollare valori come questo in Excel:

1'234567.89 

fa Excel lamentano sopra di questo valore, o cercare di conservarlo come testo? No, accetta anche felicemente questo come un numero valido e lo incolla come "1234567.89".

In ogni caso, lavoro fatto .. grazie a tutti per il loro aiuto & suggerimenti.

+0

Regex lo riporterebbe effettivamente a circa 1 o 2 linee ;-), comunque, più uno per la pubblicazione della soluzione – ArieKanarie