How to efficiently ensure a decimal value has at least N decimal places

£可爱£侵袭症+ 提交于 2019-12-23 12:27:46

问题


I want to efficiently ensure a decimal value has at least N (=3 in the example below) places, prior to doing arithmetic operations.

Obviouly I could format with "0.000######....#" then parse, but it's relatively inefficient and I'm looking for a solution that avoids converting to/from a string.

I've tried the following solution:

decimal d = 1.23M;
d = d + 1.000M - 1;
Console.WriteLine("Result = " + d.ToString()); // 1.230

which seems to work for all values <= Decimal.MaxValue - 1 when compiled using Visual Studio 2015 in both Debug and Release builds.

But I have a nagging suspicion that compilers may be allowed to optimize out the (1.000 - 1). Is there anything in the C# specification that guarantees this will always work?

Or is there a better solution, e.g. using Decimal.GetBits?

UPDATE

Following up Jon Skeet's answer, I had previously tried adding 0.000M, but this didn't work on dotnetfiddle. So I was surprised to see that Decimal.Add(d, 0.000M) does work. Here's a dotnetfiddle comparing d + 000M and decimal.Add(d,0.000M): the results are different with dotnetfiddle, but identical when the same code is compiled using Visual Studio 2015:

decimal d = 1.23M;
decimal r1 = decimal.Add(d, 0.000M);
decimal r2 = d + 0.000M;
Console.WriteLine("Result1 = " + r1.ToString());  // 1.230 
Console.WriteLine("Result2 = " + r2.ToString());  // 1.23 on dotnetfiddle

So at least some behavior seems to be compiler-dependent, which isn't reassuring.


回答1:


If you're nervous that the compiler will optimize out the operator (although I doubt that it would ever do so) you could just call the Add method directly. Note that you don't need to add and then subtract - you can just add 0.000m. So for example:

public static decimal EnsureThreeDecimalPlaces(decimal input) =>
    decimal.Add(input, 0.000m);

That appears to work fine - if you're nervous about what the compiler will do with the constant, you could keep the bits in an array, converting it just once:

private static readonly decimal ZeroWithThreeDecimals =
    new decimal(new[] { 0, 0, 0, 196608 }); // 0.000m

public static decimal EnsureThreeDecimalPlaces(decimal input) =>
    decimal.Add(input, ZeroWithThreeDecimals);

I think that's a bit over the top though - particularly if you have good unit tests in place. (If you test against the compiled code you'll be deploying, there's no way the compiler can get in there afterwards - and I'd be really surprised to see the JIT intervene here.)




回答2:


The Decimal.ToString() method outputs the number of decimal places that is determined from the structure's internal scaling factor. This factor can range from 0 to 28. You can obtain the information to determine this scaling factor by calling the Decimal.GetBits Method. This method's name is slightly misleading as it returns an array of four integer values that can be passed to the Decimal Constructor (Int32[]); the reason I mention this constructor is that its "Remarks" section of the documentation describes the the bit layout better than the documentation for the GetBits method.

Using this information you can determine the Decimal value's scale factor an thus know how many decimal places the default ToString method will yield. The following code demonstrates this as an extension method named "Scale". I also included an extension method named "ToStringMinScale" to format the Decimal to a minimum scale factor value. If the Decimal's scale factor is greater than the specified minimum, that value will be used.

internal static class DecimalExtensions
    {
    public static Int32 Scale(this decimal d)
        {
        Int32[] bits = decimal.GetBits(d);

        // From: Decimal Constructor (Int32[]) - Remarks
        // https://msdn.microsoft.com/en-us/library/t1de0ya1(v=vs.100).aspx

        // The binary representation of a Decimal number consists of a 1-bit sign, 
        // a 96-bit integer number, and a scaling factor used to divide 
        // the integer number and specify what portion of it is a decimal fraction. 
        // The scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28.

        // bits is a four-element long array of 32-bit signed integers.

        // bits [0], bits [1], and bits [2] contain the low, middle, and high 32 bits of the 96-bit integer number.

        // bits [3] contains the scale factor and sign, and consists of following parts:

        // Bits 0 to 15, the lower word, are unused and must be zero.

        // Bits 16 to 23 must contain an exponent between 0 and 28, which indicates the power of 10 to divide the integer number.

        // Bits 24 to 30 are unused and must be zero.

        // Bit 31 contains the sign; 0 meaning positive, and 1 meaning negative.

        // mask off bits 0 to 15
        Int32 masked = bits[3] & 0xF0000;
        // shift masked value 16 bits to the left to obtain the scaleFactor
        Int32 scaleFactor = masked >> 16;

        return scaleFactor;
        }

    public static string ToStringMinScale(this decimal d, Int32 minScale)
        {
        if (minScale < 0 || minScale > 28)
            {
            throw new ArgumentException("minScale must range from 0 to 28 (inclusive)");
            }
        Int32 scale = Math.Max(d.Scale(), minScale);
        return d.ToString("N" + scale.ToString());
        }

    }


来源:https://stackoverflow.com/questions/47122370/how-to-efficiently-ensure-a-decimal-value-has-at-least-n-decimal-places

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!