tags:

views:

895

answers:

10

Is there a way to get Decimal.TryParse to parse a string value of "0.0" or "00.00" or "000.000" as 0?

I have tried setting NumberStyles to Any.

+1  A: 

What is it that is not working? This works fine for me (tested with "0.0", "00.00" and "000.000"):

decimal d;
if (decimal.TryParse("0.0", NumberStyles.Any, CultureInfo.InvariantCulture, out d))
{
    // use d
}
Fredrik Mörk
The issue is that if you pass in `"0.0"`, `"0.00"` and `"0.000"` respectively and write the resulting `decimal` to the console you will see `0.0`, `0.00`, and `0.000` on the console respectively. The OP is asking to see `0` in all cases.
Jason
@Jason: I didn't interpret the question as being about formatting a decimal as a string but rather the other way around (*"Is there a way to get Decimal.TryParse to parse a string value"*), but you may be right.
Fredrik Mörk
+1  A: 

Is there a way to get Decimal.TryParse to parse a string value of "0.0" or "00.00" or "000.000" as 0?

I am interpreting your question to mean. Say I take the strings "0.0", "00.00" and "000.000" ask Decimal.TryParse to parse them. When I write out the resulting decimal to the console I see 0.0, 0.00 and 0.000 respectively. Is there a way to get Decimal.TryParse to return a decimal in all these cases that will be written to the console as 0?

No. Think about why this should be. Decimal types represent precise numbers; in certain circles, 0.00 would be considered more precise than 0.0 which would be considered more precise than 0. If Decimal.TryParse truncated that precision than the Decimal type would not be useful for these purposes.

That said, it's easy enough to just trim the trailing zeros before calling parse:

static char[] whitespaceAndZero = new[] {
    ' ',
    '\t',
    '\r',
    '\n',
    '\u000b', // vertical tab
    '\u000c', // form feed
    '0'
};
static string TrimEndWhitespaceAndZeros(string s) {
    return s.Contains('.') ? s.TrimEnd(whitespaceAndZero) : s;
}

static bool TryParseAfterTrim(string s, out decimal d) {
    return Decimal.TryParse(TrimEndWhiteSpaceAndZeros(s), out d);
}

Usage:

string s = "0.00";
decimal d;
TryParseAfterTrim(s, out d);
Console.WriteLine(d);

Output:

0

Please note that the above only shows the crux of how to solve your problem. It is up to you to decide whether or not and how you are going to handle localization issues. At a minimum, before putting this into production you should consider replacing the hard-coded '.' with CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator. You should consider having an overload of TryParseAfterTrim with the same parameter list as Decimal.TryParse. That is:

bool TryParseAfterTrim(
    string s,
    NumberStyle style,
    IFormatProvider provider,
    out decimal result
)
Jason
@ChrisF: The trailing decimal point is irrelevant. `Decimal.TryParse` will correctly parse `"0.`"
Jason
`TrimTrailingZeros` is slightly misleading here because `Trim` will trim leading zeros as well.
Austin Salonen
@Austin Salonen: You're right; I have given it a better name. Thanks.
Jason
@Downvoter: Please explain! Thanks.
Jason
I get your point about precision, but if you're going to throw away trailing zeros, I think you ought to do it on output -- and it's trivially easy to do with a format string.
tvanfosson
...and FWIW, it wasn't my downvote...
tvanfosson
Bad solution with a big bug inside! If you parse eg "10" with your TrimZero the result will be 1, which is dead wrong.You should submit this answer to Dailywtf.com
Sam
Of course it should be thedailywtf.com
Sam
What about `return s.Contains(".") ? s.Trim('0') : s;` for your helper function?
Jarrod Dixon
Fixed one bug, added a new one:Your new solution does work ... in the US. In european countries often a colon is used instead of the dot for fractions. So it would be "0,00" instead of "0.00". A program relying on your method will break in lotsa funny ways then.
Sam
@Sam: How to deal with localization for the above is a separate issue; if the OP is not expecting input from Europeans, or does not want to parse such input then it is not a bug. That is, if the specification of the method is "parse decimals formatted as US-localized strings into decimals but truncate trailing zeros after the decimal point" then what I have is fine.
Jason
@Downvoters: Please explain. Thanks!
Jason
"this kind of input won't happen" is not a valid excuse for introducing bugs into a system. Especially so when you don't know where the OP wants to use the function.
Sam
"Downvoters: Please explain" - in my case it's the localization issue pointed out by Sam. In the unlikely event that the spec mandates US-localized strings, you should be explicitly using that culture for parsing. A robust general solution would have an overload that accepts an IFormatProvider, and default to CultureInfo.CurrentCulture.
Joe
@Sam: If the specification of the method is "parse decimals formatted as US-localized strings into decimal but truncate trailing zeros after the decimal point" then parsing `"0,00"` as the decimal `0` would be a bug. Rather, the correct behavior would be to report an input error on such input. And regarding your comment "[e]specially so when you don't know where the OP wants to use the function. " Touche and right back at you. Which is why I previously said dealing with localization is an issue separate from this one.
Jason
@Joe: Don't needlessly generalize problems. YAGNI and all that. The OP can worry about how to turn the above into production-quality code. Also, please see my comment immediately prior to this one.
Jason
@Jason: I respectfully disagree. Your code is fragile because it using a mixture of a hardwired decimal separator ('.') and implicitly parsing using the current culture. Thus in Germany, your code will parse "1.234" as 1.234 instead of 1234 (period is a thousands separator). And expecting the OP to detect and fix this bug without at least pointing it out is a bit much. YAGNI doesn't mean that a fragile solution is acceptable IMHO.
Joe
@Joe: Again, it's up to the OP how best to turn the above into production-quality code. I do agree with you that it's worth pointing out to the OP the potential localization issues; it's my experience that people are, in general, not cognizant enough of these issues. Thanks for the comments; I will edit accordingly.
Jason
Look, while I too spotted the bug where your original version of `TrimZeros` would turn 100 into 1, the more fundamental issue here (as I see it) is that you're using *string representations* as your back-end logic to manipulating numbers. There's a very basic problem there, and you're going to run into unforeseen issues on top of what others have already mentioned. Why not just check if the parsed number == 0M, and if so return 0M?
Dan Tao
@Dan: Because that would horribly fragile. What if the OP also wants `"1.00"` to be parsed as `1`? I see no issue with doing string-manipulations in a string-parsing problem.
Jason
@Jason: I don't agree that it would be "fragile." It wouldn't cause any *incorrect* behavior; it would simply not deal with nonexistent requirements. (And you yourself made mention of YAGNI just a few comments back.) String manipulation, on the other hand, is *extremely* fragile. For instance you may feel that you've dealt with the 100 -> 1 bug by adding the `Contains('.')` check, but what about the string 0.0? `Trim(new[] { '0' })` will trim on both sides, resulting in the string `.` in which case `TryParseAfterTrim` will fail and return false.
Dan Tao
In other words the reason the code seems to have "worked" in your example is that `TryParseAfterTrim` *failed*, set `d` to `default(decimal)` (which is 0M), and returned false. If you don't believe me, try changing the code to `if (TryParseAfterTrim(s, out d)) Console.WriteLine(d);` You'll find nothing is printed.
Dan Tao
@Jason: Now you've "fixed" the issue I pointed out above by converting `Trim` to `TrimEnd`, admittedly an improvement. However, in my opinion, your code is still broken. The string ".000", which `decimal.TryParse` will correctly parse as 0(.000) -- a numerical value easily convertible to the desired 0 -- will not be parsed properly by your `TryParseAfterTrim` method because it will first be trimmed to "." which is not a number. Again, from my perspective, this demonstrates the fragility in your approach of using string manipulation to handle numbers.
Dan Tao
@Dan: Either that is a bug in `Decimal.TryParse` or the documentation is not correct. According to http://msdn.microsoft.com/en-us/library/9zbda557.aspx the string parameter to be parsed should be formatted as `[ws][sign][digits,]digits[.fractional-digits][ws]`. Note that there is a non-optional non-terminal `digits` before the optional decimal point. Looking at the documentation, it appears that whitespace is also legal on the end and therefore I have adjusted for that too.
Jason
@Jason, you fix your bugs in hasty ways, adding new on the way. And now you try and make your bugs go away by calling names. This is especially bad since SO is meant as knowledge repository, and you never know who is going to read your "solution". You don't even know where the OP wants to use it, you just imply US.
Sam
+11  A: 

Using the InvariantCulture, decimal.TryParse does successfully interpret all of those strings as zero. This code, for example:

decimal num = 66;
foreach (var str in new string[] { "0.0", "00.00", "000.000" })
{
    if (!decimal.TryParse(str, out num))
    {
        Console.WriteLine( "fail" );
    }
    Console.WriteLine(num);
}

Produces this output:

0.0
0.00
0.000

Perhaps, the issue is printing the values, not parsing the values? If that's the case, simply use a format string that specifies that the decimal places are optional.

Console.WriteLine( num.ToString( "#0.#" ) );

Produces

0
0
0
tvanfosson
Note that you have hardcoded `"0.0"` in the call to `TryParse`. I am sure that if you passed in `str` you would see `0.0`, `0.00` and `0.000` on the console.
Jason
@Jason -- you're correct. I've updated. The problem isn't the conversion to decimal but the conversion back to a string on printing. All of those values are still zero. I've updated my answer to reflect this.
tvanfosson
@tvanfosson: That's right, they all represent zero, but they represent different levels of precision of zero.
Jason
@Jason -- since he want's them to all reflect the same precision, I'm assuming that precision is less important than consistently printing the zero value the same way.
tvanfosson
I agree that you should format on display; never throw away what was inputted! Same principle we use here on SO when saving a post's body - store the raw input, encode/sanitize on display.
Jarrod Dixon
If the requirement is more generally "remove trailing zeroes from any decimal value while retaining all significant digits" (not just the three examples from the OP), then this solution doesn't work, as it rounds the result to a maximum of one decimal. Instead you could use a format string like G28 or 0.############################ as described in my answer.
Joe
@Joe -- noted. You should use as many hash/pounds signs as you need significant digits.
tvanfosson
A: 

In your Console.Writeline, apply a string format to maintain the double zero precision.

Jonathan Bates
A: 

I see what you mean. The following code:

decimal number = -1.0M;
bool res1 = decimal.TryParse("0.0", out number);
Console.WriteLine(string.Format("number = {0}", number));
number = -1.0M;
bool res2 = decimal.TryParse("00.00", out number);
Console.WriteLine(string.Format("number = {0}", number));
number = -1.0M;
bool res3 = decimal.TryParse("000.000", out number);
Console.WriteLine(string.Format("number = {0}", number));
number = -1.0M;
bool res4 = decimal.TryParse("0000.0000", out number);
Console.WriteLine(string.Format("number = {0}", number));

prints:

number = 0.0
number = 0.00
number = 0.000
number = 0.0000

So the input does affect the output. But checking the value of number using the debugger shows "0" in the tooltip and Locals window which indicates that it's a formatting issue rather than a fundamental problem with the value being stored.

However, as others have said trim the trailing zeros (and decimal point) before converting.

ChrisF
I did the same experiment. I wonder if this is for some scientific app where significant figures is coming into play...
Austin Salonen
+1  A: 

// This outputs 0 given 0.0 as a string though....

 decimal d;

 decimal.TryParse("0.0", out d);

 string s = String.Format("{0:0}", d);
gmcalab
+2  A: 

Preserving trailing zeroes in decimal values was introduced in .NET 1.1 for more strict conformance with the ECMA CLI specification. See this answer to a similar question.

While the original question is limited to removing trailing zeroes when parsing a string, I think it's important to understand why and under what circumstances trailing zeroes are preserved in a decimal value's internal representation.

Trailing zeroes can appear in a decimal value as a result of :

a) parsing input that has trailing zeroes, as in the original post, or

b) as a result of a calculation. For example: multiplying decimal values 1.2 * 1.5 gives a result of 1.80: this is because multiplying two values that are each accurate to one decimal place gives a result that is accurate to two decimal places - and the second decimal place is therefore preserved even if its value is zero.

What to do about trailing zeroes in the internal decimal representation? In general do nothing: you can format on output to your desired number of decimals, so they won't hurt.

Decimal is mainly used for financial calculations, and it's generally desirable to preserve trailing zeroes so that an amount rounded to the nearest cent is represented as 1.20 rather than 1.2. Normally when using decimals, you will be working to a fixed precision (perhaps to the nearest cent in a retail application, or to the nearest hundredth of a cent when calculating mobile phone usage charges). So your application logic will explicitly take care of rounding to a fixed number of decimals using explicit rounding rules, e.g. a tax calculation that rounds to the nearest cent, using the MidpointRounding.AwayFromZero:

decimal price = 1.20M;   
decimal taxRate = 0.175; // 17.5%
decimal taxAmount = Math.Round(price*taxRate, 2, 
                      MidpointRounding.AwayFromZero);

If you're not working to a fixed number of decimals in this way, you might consider whether you should be using double rather than decimal.

Nevertheless, there may occasionally be a requirement to remove trailing zeroes, while retaining all significant digits. AFAIK, there isn't a built-in method for this.

If you need to do so, a basic solution would be to format as a string using the standard format string "G28" (equivalent to the custom format string "0.############################"), then parse the result back to a decimal.

An alternative way of removing trailing zeroes without converting to/from a string (probably faster, though I haven't measured it) is to use Math.Round - e.g. the following method:

    static decimal RemoveTrailingZeroes(decimal value)
    {
        const int MaxDecimals = 28;
        decimal roundedValue;
        for (int decimals = 0; decimals < MaxDecimals; decimals++)
        {
            roundedValue = Math.Round(value, decimals);
            if (value == roundedValue) return roundedValue;
        }
        return value;
    }
Joe
A: 

One way to get rid of trailing zeros after decimal point would be eliminating them in the decimal.

The brute force way would be meddling with the internal structure of the decimal:

public static Decimal TrimFractionZeros(this Decimal zahl)
{
  int[] bits = decimal.GetBits(zahl);
  byte decimals = (Byte)(bits[3] >> 16);
  bool negativ = (bits[3] >> 31) != 0;
  zahl = new decimal(bits[0], bits[1], bits[2], false, 0);
  while ((decimals > 0) && ((zahl % 10) == 0))
  {
    zahl /= 10;
    decimals--;
  }
  bits = decimal.GetBits(zahl);
  return new decimal(bits[0], bits[1], bits[2], negativ, decimals);
}

Not pretty, not fast, but it will change the internal representation of "0.000" to "0" in a decimal.

Sam
A: 

Why don't you use int.TryParse? or double.TryParse?

Erik Van Hecke
+2  A: 

I humbly submit this solution:

    decimal processedValue = value == 0 ? 0 : value;

Does this not do the job?

Here is a complete example:

string valueString = "0.000";
decimal value;
bool isValid = decimal.TryParse(valueString, out value);
if (isValid)
{
    decimal processedValue = value == 0 ? 0 : value;
    System.Diagnostics.Debug.Print("value: {0}, processedValue: {1}", value, processedValue);
}
DanM