Truncate Two decimal places without rounding

216,508

Solution 1

value = Math.Truncate(100 * value) / 100;

Beware that fractions like these cannot be accurately represented in floating point.

Solution 2

It would be more useful to have a full function for real-world usage of truncating a decimal in C#. This could be converted to a Decimal extension method pretty easy if you wanted:

public decimal TruncateDecimal(decimal value, int precision)
{
    decimal step = (decimal)Math.Pow(10, precision);
    decimal tmp = Math.Truncate(step * value);
    return tmp / step;
}

If you need VB.NET try this:

Function TruncateDecimal(value As Decimal, precision As Integer) As Decimal
    Dim stepper As Decimal = Math.Pow(10, precision)
    Dim tmp As Decimal = Math.Truncate(stepper * value)
    Return tmp / stepper
End Function

Then use it like so:

decimal result = TruncateDecimal(0.275, 2);

or

Dim result As Decimal = TruncateDecimal(0.275, 2)

Solution 3

Universal and fast method (without Math.Pow() / multiplication) for System.Decimal:

decimal Truncate(decimal d, byte decimals)
{
    decimal r = Math.Round(d, decimals);

    if (d > 0 && r > d)
    {
        return r - new decimal(1, 0, 0, false, decimals);
    }
    else if (d < 0 && r < d)
    {
        return r + new decimal(1, 0, 0, false, decimals);
    }

    return r;
}

Solution 4

Use the modulus operator:

var fourPlaces = 0.5485M;
var twoPlaces = fourPlaces - (fourPlaces % 0.01M);

result: 0.54

Solution 5

One issue with the other examples is they multiply the input value before dividing it. There is an edge case here that you can overflow decimal by multiplying first, an edge case, but something I have come across. It's safer to deal with the fractional part separately as follows:

    public static decimal TruncateDecimal(this decimal value, int decimalPlaces)
    {
        decimal integralValue = Math.Truncate(value);

        decimal fraction = value - integralValue;

        decimal factor = (decimal)Math.Pow(10, decimalPlaces);

        decimal truncatedFraction = Math.Truncate(fraction * factor) / factor;

        decimal result = integralValue + truncatedFraction;

        return result;
    }
Share:
216,508
Admin
Author by

Admin

Updated on November 17, 2021

Comments

  • Admin
    Admin over 2 years

    Lets say I have a value of 3.4679 and want 3.46, how can I truncate to two decimal places that without rounding up?

    I have tried the following but all three give me 3.47:

    void Main()
    {
        Console.Write(Math.Round(3.4679, 2,MidpointRounding.ToEven));
        Console.Write(Math.Round(3.4679, 2,MidpointRounding.AwayFromZero));
        Console.Write(Math.Round(3.4679, 2));
    }
    

    This returns 3.46, but just seems dirty some how:

    void Main()
    {
        Console.Write(Math.Round(3.46799999999 -.005 , 2));
    }
    
  • driis
    driis almost 14 years
    Use decimal for your values and this answer will work. It is unlikely to always work in any floating point representation.
  • Admin
    Admin almost 14 years
    That makes me wonder whether it should be possible to specify rounding direction in floating point literals. Hmmmm.
  • David Airapetyan
    David Airapetyan over 10 years
    Actually, hardcoding '.' is not a good idea, better use System.Globalization.CultureInfo.CurrentCulture.NumberFormat‌​.NumberDecimalSepara‌​tor[0]
  • TheKingDave
    TheKingDave over 10 years
    I know this is old but I noticed and issue with this. The factor you have here is an int and so if you are truncating to a large number of decimal places (say 25) it will cause the end result to have precision error. I fixed it by changing the factor type to decimal.
  • Ignacio Soler Garcia
    Ignacio Soler Garcia over 10 years
    @TheKingDave: probably it's irrelevant but as factor cannot have decimals should be better to model it as long right?
  • TheKingDave
    TheKingDave over 10 years
    @SoMoS For me Decimal worked better because it gave me the highest storage values for factor. It still has a limitation but it is big enough for my application. Long on the other hand wasn't able to store large enough numbers for my application. For example if you do a Truncate(25) with long then there will be some inaccuracy.
  • Tim Lloyd
    Tim Lloyd over 10 years
    Updated to allow truncation to a greater number of places as per @TheKingDave suggestion, thanks.
  • nightcoder
    nightcoder almost 8 years
    This will overflow on large numbers.
  • user1703401
    user1703401 almost 8 years
    There has to be some way to tell the programmer that calculating with the assumption that a number can store more than 308 digits is grossly inappropriate. Double can store only 15. Overflow is very much a feature here, it overflowed rather badly.
  • nightcoder
    nightcoder almost 8 years
    I'm sorry, I thought "value" is decimal.
  • Gqqnbig
    Gqqnbig over 7 years
    I don't like suffixing "Ex" to it. C# supports overloadding, your Truncate method will be groupped together with .net native ones, giving user a seamless experience.
  • Jon Senchyna
    Jon Senchyna over 7 years
    Your algorithm results in some incorrect results. The default MidpointRounding mode is Banker's Rounding, which rounds 0.5 to the nearest even value. Assert.AreEqual(1.1m, 1.12m.TruncateEx(1)); fails because of this. If you specify "normal" rounding (AwayFromZero) in the Math.Round call, then Assert.AreEqual(0m, 0m.TruncateEx(1)); fails
  • Jon Senchyna
    Jon Senchyna over 7 years
    The only way this solution will work is if you use MidpointRounding.AwayFromZero and specifically code to handle the value 0.
  • nightcoder
    nightcoder over 7 years
    "Assert.AreEqual(1.1m, 1.12m.TruncateEx(1)); fails" - What do you mean "fails"? This code has been tested on .NET 4, 4.5.2 & 4.6.1. I use it at work and in my personal projects, and the unit test in my answer is executed every day. And it passes. Am I missing something? Could you provide a screenshot or exception details if my test fails on your machine?
  • Ho Ho Ho
    Ho Ho Ho about 7 years
    Jon is correct: 0m.TruncateEx(0) results in -1 unless 0 is explicitly handled. Likewise -11m.TruncateEx(0) results in -10 unless MidpointRounding.AwayFromZero is used within Math.Round. Seems to work well with those modifications though.
  • Ho Ho Ho
    Ho Ho Ho about 7 years
    Even with changes for AwayFromZero and explicit handling of 0, -9999999999999999999999999999m.TruncateEx(0) results in -9999999999999999999999999998, so it is still fallible in some cases.
  • Sarel Esterhuizen
    Sarel Esterhuizen about 7 years
    To add to night coder, the fact that you are using Int32 as intermediary in your function will cause overflows. You should use Int64 if you really must cast it to an Integer. The question would be why you would want to incur that extra overhead anyway since Truncate returns Decimal integrals anyway. Just do something like: decimal step = (decimal)Math.Pow(10, precision); return Math.Truncate(step * value) / step;
  • Corgalore
    Corgalore about 7 years
    I dropped the cast to Integer. I left them separate lines for better readability and understanding of how the function works.
  • RichardOD
    RichardOD about 6 years
    I ran this through all of the tests mentioned in the other answers and it works perfectly. Surprised it doesn't have more upvotes. It is worth noting that decimals can only be between 0 and 28 (Probably OK for most people).
  • Branko Dimitrijevic
    Branko Dimitrijevic almost 6 years
    I second that. This is the best answer. +1
  • Nic3500
    Nic3500 over 5 years
    While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value.
  • Isaac Baker
    Isaac Baker almost 5 years
    I don't understand (read: didn't spend the time to verify) all these other fancy solutions, this does exactly what I was looking for. Thank you!
  • bruno.almeida
    bruno.almeida over 4 years
    Great answer, that's what i call "think outside of the box"
  • AndrewBenjamin
    AndrewBenjamin over 4 years
    I had to upvote because of all the thought and effort. You set Nesterov's as a benchmark and kept going - hats off.
  • ttugates
    ttugates over 3 years
    Running this on .Net Fiddle clicky produces 0.5400... The answer by D. Nesterov below produced the expected 0.54.
  • Leonard Lewis
    Leonard Lewis over 3 years
    You do realize, @ttugates , that 0.54 and 0.5400 are exactly the same value, right? It doesn't matter how many zeros follow unless/until it comes time to format for display—in which case, the result will be the same if formatted properly: $"{0.54m:C}" produces "$0.54" and yes, $"{0.5400m:C}" produces "$0.54".
  • Deepak
    Deepak over 3 years
    Good answer. This works with double data type too, if i am able to hard code the value for "new decimal(1, 0, 0, false, decimals)" ;)
  • thomasgalliker
    thomasgalliker over 3 years
    How do I have to call this method? var truncated = Truncate(decimalNumber, (byte)decimalPlaces); where decimalNumber is of type decimal and decimalPlaces is of type int. Is this correct?
  • Propagating
    Propagating over 3 years
    @thomasgalliker it depends on how you implement it, but if you just leave it as its own method instead of an extension method then yes, that's fine.
  • Propagating
    Propagating over 3 years
    From some very basic benchmarking this is ~35% faster than methods using Math.Pow and division. 1.119 seconds to do 10,000,000 truncations using Math.Pow vs 0.707 seconds using this method.
  • JoeTomks
    JoeTomks about 3 years
    This is a bit of a lazy answer, it should be updated to actually show you returning the value to two decimal places which as pointed out this code doesn't do. If we are looking at completeness and being pedantic, which well I am, this is only a partial answer granted it does answer the issue of removing the rounding component but again, not technically complete.
  • princessbubbles15
    princessbubbles15 about 3 years
    i like this answer. simple but works very well
  • princessbubbles15
    princessbubbles15 about 3 years
    i like this one also :D
  • Neph
    Neph over 2 years
    According to the docs, there's no need to distinguish between positive and negative with ToZero: 2.8 and 2.1 both truncate to 2 with it and for -2.8 and -2.1 the result is -2.
  • Neph
    Neph over 2 years
    Important: You do need .NET 5.0 for this though, with version 4.0 you can only use AwayFromZero and ToEven.
  • Panagiotis Kanavos
    Panagiotis Kanavos over 2 years
    @Neph this is available since .NET Core 3. There never was a .NET Core 4. .NET 5 is .NET Core 5. The old .NET Framework stopped at 4.8
  • Neph
    Neph over 2 years
    I'm talking about the "regular" .NET 5.0 release (not sure how it's connected to "Core",...). I mentioned this because Unity 2021 only supports the ".NET Standard 2.0" and ".NET 4.x" C# and ToZero or even the other two unfortunately aren't supported.
  • Panagiotis Kanavos
    Panagiotis Kanavos over 2 years
    @Neph there's no "regular". .NET 5 is the current version of .NET Core 5. The name changed for marketing purposes. It includes almost all of the .NET Old APIs that were going to be migrated to .NET Core, so a decision was made to "unify" the platforms by ... unifying the names
  • Panagiotis Kanavos
    Panagiotis Kanavos over 2 years
    @Neph check Introducing .NET 5. The first line is Today, we’re announcing that the next release after .NET Core 3.0 will be .NET 5. This will be the next big release in the .NET family.. The reasoning is explained in this Github issue - no matter what name was used, there would be trouble and confusion. I don't like the new naming either. At least, it's easier to sell a change from .NET Framewok 4.8 to .NET 5.0 to non-technical managers
  • Neph
    Neph over 2 years
    Their naming system is quite confusing, you're right. Check the "Applies to" section in the docs I linked, all the versions that support ToZero are listed there. It's weird, Unity 2021's C# is ".NET Standard 2.0", which is listed but it still doesn't support that enum - I guess they're doing their own thing again and even though there are plans to update to .NET 6 (they'll skip 5), it might still be a while.
  • Panagiotis Kanavos
    Panagiotis Kanavos over 2 years
    @Neph The Applies To section applies to the type, not its values. To see what's available in different versions change the version from the Version dropdown at the top left. You'll see that the new values aren't available even in .NET Standard 2.1.
  • Syed Irfan Ahmad
    Syed Irfan Ahmad over 2 years
    Calculation in catch block is producing extra zeros after two decimal places if "precision" is 2