简体   繁体   中英

C# string to Decimal On All style or Culture

Hi I want to find if there is any better way to parse the string to Decimal which covers various format

  1. $1.30
  2. £1.50
  3. €2,50
  4. 2,50 €
  5. 2.500,00 €

I see a lot of examples using culture to convert . & , . But in my case, I don't have anything to identify the culture.

This display field I get from the client and I need to extract the value.

I tried following (which didn't work for all scenario) but would like to know if we have any best way to handle this.

Decimal.Parse(value,NumberStyles.Currency |
                    NumberStyles.Number|NumberStyles.AllowThousands |
                    NumberStyles.AllowTrailingSign | NumberStyles.AllowCurrencySymbol)

I also tried to use Regex to remove the currency sign but unable to convert both 1.8 or 1,8 in one logic.

Well, assuming you always get a valid currency format, and it's only the culture that changes, you could guess which character is used as a decimal point and which is used as a thousands separator by checking which appears the last in the number. Then remove all the thousand separators and parse it like its culture was invariant.

The code would look like the following:

// Replace with your input
var numberString = "2.500,00  €";

// Regex to extract the number part from the string (supports thousands and decimal separators)
// Simple replace of all non numeric and non ',' '.' characters with nothing might suffice as well
// Depends on the input you receive
var regex = new Regex"^[^\\d-]*(-?(?:\\d|(?<=\\d)\\.(?=\\d{3}))+(?:,\\d+)?|-?(?:\\d|(?<=\\d),(?=\\d{3}))+(?:\\.\\d+)?)[^\\d]*$");

char decimalChar;
char thousandsChar;

// Get the numeric part from the string
var numberPart = regex.Match(numberString).Groups[1].Value;

// Try to guess which character is used for decimals and which is used for thousands
if (numberPart.LastIndexOf(',') > numberPart.LastIndexOf('.'))
{
    decimalChar = ',';
    thousandsChar = '.';
}
else
{
    decimalChar = '.';
    thousandsChar = ',';
}

// Remove thousands separators as they are not needed for parsing
numberPart = numberPart.Replace(thousandsChar.ToString(), string.Empty);

// Replace decimal separator with the one from InvariantCulture
// This makes sure the decimal parses successfully using InvariantCulture
numberPart = numberPart.Replace(decimalChar.ToString(),
    CultureInfo.InvariantCulture.NumberFormat.CurrencyDecimalSeparator);

// Voilá
var result = decimal.Parse(numberPart, NumberStyles.AllowDecimalPoint | NumberStyles.Number, CultureInfo.InvariantCulture);

It does look a bit of complicated for a simple decimal parsing, but I think should do the work for all the input numbers you get or at least the most of them.

If you do this in some sort of loop, you might want to use compiled regex.

The only way you could do that is just strip string from symbols and change . and , to decimal separator. Something like:

public decimal UniversalConvertDecimal(string str)
{
    char currentDecimalSeparator = Convert.ToChar(Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator);

    str = str.Replace('.', currentDecimalSeparator);
    str = str.Replace(',', currentDecimalSeparator);

    StringBuilder builder = new StringBuilder(str.Length);
    foreach(var ch in str)
    {
       if(Char.IsDigit(ch) || ch == currentDecimalSeparator)
         builder.Add(ch);             
    }

    string s = builder.ToString();
    return Convert.ToDecimal(s);

}

First you have to get current decimal separator from your system. Then you have to replace . and , with current decimal separator. Next, you will have to strip the string from any other char than a digit or decimal separator. At the end you can be sure that Convert.ToDecimal is going to work. But I don't know if it is something you want to achieve.

If you need some mechanism to save currency to database, there is a far simpler solution. Just convert this currency to least currency part. For example instead of $1, save 100 cents.

So if you have $1.99, just multiply it by 100 and you will get: 199 cents. And this integer can be saved to db.

The problem here is that in one case . means decimal point but in other it is a thousnads separator. And then you have , as decimal separator. Clearly, it is impossible for the parser to "guess" what is meant, so the only thing you can do is to decide on some rules on how to handle which case.

If you have control over the UI the best approach would be to validate user input and just reject any value that can't be parsed with an explanation on which format is expected.

If you have no control over the UI, the second best option would be to check for some "rules" and then devise which culture is appropriate for that given input and try to run it through decimal.TryParse for that given culture.

For the given input you have, you could have the following rules:

  • input.StartsWith("$") -> en-US
  • input.StartsWith("£") -> en-GB
  • input.StartsWith("€") || input.EndsWith("€") -> de-DE

These could reasonably handle all cases.

In code:

static void Main(string[] args)
{
    string[] inputs =
    {
        "$1.30",
        "£1.50",
        "€2,50",
        "2,50 €",
        "2.500,00 €"
    };
    for (int i = 0; i < inputs.Length; i++)
    {
        Console.Write((i + 1).ToString() + ". ");
        if (decimal.TryParse(inputs[i], NumberStyles.Currency,
                             GetAppropriateCulture(inputs[i]), out var parsed))
        {
            Console.WriteLine(parsed);
        }
        else
        {
            Console.WriteLine("Can't parse");
        }
    }
}

private static CultureInfo GetAppropriateCulture(string input)
{
    if (input.StartsWith("$")) 
        return CultureInfo.CreateSpecificCulture("en-US");
    if (input.StartsWith("£")) 
        return CultureInfo.CreateSpecificCulture("en-GB");
    if (input.StartsWith("€") || input.EndsWith("€")) 
        return CultureInfo.CreateSpecificCulture("de-DE");
    return CultureInfo.InvariantCulture;
}

Output:

  1. 1.30
  2. 1.50
  3. 2.50
  4. 2.50
  5. 2500.00

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM