Decimal.TryParse с радостью принимает сильно отформатированные строки чисел

Есть ли способ сделать функции С# TryParse() немного более строгими?

Прямо сейчас, если вы передаете строку, содержащую числа, правильные десятичные и тысячи разделительных символов, она часто кажется, что их принимают, даже если формат не имеет смысла, например: 123''345'678

Я ищу способ сделать TryParse не успешным, если число не в нужном формате.

Итак, я нахожусь в Цюрихе, и если я это сделаю:

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

... тогда, с моими региональными настройками, я получаю это...

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

Итак, вы можете видеть, что для моей области знак десятичной точки является полной остановкой, а тысячный разделитель - апострофом.

Теперь давайте создадим простую функцию, чтобы проверить, можно ли анализировать string в 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));
}

Хорошо, позвольте этой функции вывести несколько строк.

Какую из следующих строк вы считаете успешной обработкой этой функции?

Ниже приведены результаты, полученные мной:

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"

Я думаю, вы можете видеть мою мысль.

Мне нужны только первые две строки. Остальные должны были потерпеть неудачу, так как они не имеют 3-значных цифр после тысячи разделителей или имеют два апострофа вместе.

Даже если я изменю ParseTest, чтобы быть более конкретным, результаты будут точно такими же. (Например, он с радостью принимает "123''345'678" как допустимое десятичное число.)

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

Итак, есть ли простой способ не допускать принятие отформатированных строк с помощью TryParse?

Обновление

Спасибо за все предложения.

Возможно, я должен уточнить: то, что я ищу, - это то, что первые две из этих строк являются действительными, а третье - для отклонения.

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

Конечно, должен быть способ использовать "NumberStyles.AllowThousands", поэтому он может дополнительно разрешить тысячи разделителей, но убедиться, что формат номера имеет смысл?

Прямо сейчас, если я использую это:

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

Получаю следующие результаты:

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

И если я использую это:

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

Получаю следующие результаты:

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

Это моя проблема... независимо от настроек CultureInfo, эта третья строка должна быть отклонена, а первые два приняты.

Ответ 1

Самый простой способ определить, правильно ли он отформатирован на основе текущей культуры, - сравнить полученное число после форматирования с исходной строкой.

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

Это будет полезно для входов, которые полностью пропускают тысячи разделителей и входов, которые задают правильные разделители тысяч.

Если вам нужно принять диапазон десятичных знаков, вам нужно будет захватить число символов после десятичного разделителя и добавить его в строку формата "N".

Ответ 2

Это потому, что парсинг просто пропускает строку NumberFormatInfo.NumberGroupSeparator и полностью игнорирует свойство NumberFormatInfo.NumberGroupSizes. Однако вы можете реализовать такую ​​проверку:

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

Он еще не полностью совершенен, поскольку MSDN говорит:

Первый элемент массива определяет количество элементов в наименее значимой группе цифр сразу слева от NumberDecimalSeparator. Каждый последующий элемент ссылается на следующую значительную группу цифр слева от предыдущей группы. Если последний элемент массива не равен 0, остальные цифры сгруппированы на основе последнего элемента массива. Если последний элемент равен 0, остальные цифры не группируются.

Например, если массив содержит {3, 4, 5}, цифры сгруппированы аналогично "55 555555 555555 555555 444 433,00". Если массив содержит {3, 4, 0}, цифры сгруппированы аналогично "55555555555555555,4444,333.00".

Но теперь вы можете видеть точку.

Ответ 3

Составляя все полезные предложения здесь, вот что я в итоге использовал.

Это не идеально, но для моего корпоративного приложения он, по крайней мере, отклоняет числовые строки, которые "не выглядят правильно".

Прежде чем представить свой код, вот различия между тем, что примет моя функция TryParseExact, и что будет принимать обычный decimal.TryParse:

введите описание изображения здесь

И вот мой код.

Я уверен, что есть более эффективный способ сделать это, используя regex или что-то в этом роде, но этого достаточно для моих нужд, и я надеюсь, что это поможет другим разработчикам:

    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 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 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 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, я создал изображение таблицы выше в Excel и заметил, что на самом деле трудно вставить такие значения в Excel:

1'234567.89

Предоставляет ли Excel жалобу выше этого значения или пытается сохранить ее как текст? Нет, он также счастливо принимает это как действительное число и вставляет его как "1234567.89".

В любом случае, работа выполнена.. благодаря всем за их помощь и предложения.