如何有效地确保十进制值至少有N个小数位

时间:2017-11-05 13:55:07

标签: c# .net language-specifications

我想在进行算术运算之前,有效地确保十进制值至少有N(在下面的例子中为3)。

显然我可以使用"0.000######....#"进行格式化然后解析,但效率相对较低,而且我正在寻找避免转换为字符串/从字符串转换的解决方案。

我尝试过以下解决方案:

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

在Debug和Release版本中使用Visual Studio 2015进行编译时似乎适用于所有值< = Decimal.MaxValue - 1

但我怀疑编译器是否可以优化(1.000 - 1)。 C#规范中有什么可以保证它始终有效吗?

或者是否有更好的解决方案,例如使用Decimal.GetBits

更新

跟随Jon Skeet的回答,我之前曾尝试添加0.000M,但这对dotnetfiddle不起作用。所以我很惊讶地看到Decimal.Add(d, 0.000M)确实有效。 Here's a dotnetfiddle比较d + 000Mdecimal.Add(d,0.000M):dotnetfiddle的结果不同,但使用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

因此,至少某些行为似乎依赖于编译器,这并不令人放心。

2 个答案:

答案 0 :(得分:6)

如果您对编译器将优化运算符感到紧张(尽管我怀疑它会这样做),您可以直接调用Add方法。请注意,您不需要添加然后减去 - 您只需添加0.000米即可。例如:

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

这似乎工作正常 - 如果你对编译器对常量做什么感到紧张,你可以将这些位保留在一个数组中,只转换一次:

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

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

我认为这有点过头了 - 特别是如果你有好的单元测试。 (如果您对要编译的已编译代码进行测试,那么编译器之后就无法进入 - 并且我真的真的惊讶地看到JIT介入此处。)

答案 1 :(得分:0)

Decimal.ToString()方法输出由结构的内部缩放因子确定的小数位数。此因子的范围为0到28.您可以通过调用Decimal.GetBits Method来获取信息以确定此缩放因子。这个方法的名称有点误导,因为它返回一个包含四个整数值的数组,可以传递给Decimal Constructor (Int32[]);我提到这个构造函数的原因是文档的“备注”部分描述了比GetBits方法的文档更好的位布局。

使用此信息可以确定十进制值的比例因子,从而知道默认ToString方法将产生多少小数位。以下代码将此演示为名为“Scale”的扩展方法。我还包括一个名为“ToStringMinScale”的扩展方法,将Decimal格式化为最小比例因子值。如果Decimal的比例因子大于指定的最小值,则将使用该值。

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

    }