截断小数点后两位,不进行四舍五入。

150
假设我有一个值为3.4679,想要保留两位小数,而不进行四舍五入,该如何截断呢?
我尝试了以下三种方法,但都得到了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));
}

这个返回3.46,但总觉得有点不太干净。
void Main()
{
    Console.Write(Math.Round(3.46799999999 -.005 , 2));
}
25个回答

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

请注意,像这样的分数无法在浮点数中精确表示。


14
使用十进制表示您的数值,这个答案就能够起作用。在任何浮点表示中都不太可能总是有效。 - driis
2
这让我想知道是否可以在浮点数字面量中指定舍入方向。嗯嗯。 - user180247
在这种情况下,Math.Truncate(100 * -100.999) / 100 将不起作用。结果为-101。 - Diogo Cid

65

在实际应用中,截断C#中的小数更具有实用性。如果你想要,这可以很容易地转换为Decimal扩展方法:

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

如果你需要VB.NET,可以尝试这个:

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

然后像这样使用:

decimal result = TruncateDecimal(0.275, 2);

或者
Dim result As Decimal = TruncateDecimal(0.275, 2)

5
这会在大数值时溢出。 - nightcoder
1
除了夜间编码器之外,您在函数中使用Int32作为中介会导致溢出。如果您真的必须将其转换为整数,则应使用Int64。问题是为什么您要承担额外的开销,因为Truncate返回十进制积分。只需执行以下操作:decimal step = (decimal)Math.Pow(10, precision); return Math.Truncate(step * value) / step; - Sarel Esterhuizen
我取消了对整数的强制转换。我将它们分开成不同的行以便更好地阅读和理解函数的工作原理。 - Corgalore

65

对于 System.Decimal,一种通用且快速的方法(不使用 Math.Pow() / 乘法):

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

6
我已将此内容运行通过其他答案提及的所有测试,并且它完美地工作了。很惊讶它没有更多的赞。值得注意的是,小数只能在0到28之间(对大多数人来说可能足够了)。 - RichardOD
1
我赞同。这是最好的答案。+1 - Branko Dimitrijevic
1
很棒的答案,这就是我所说的“跳出思维定势”的表现。 - bruno.almeida
@thomasgalliker 这取决于你如何实现它,但如果你只将它作为自己的方法而不是扩展方法,那么没问题。 - Propagating
从一些非常基本的基准测试来看,这种方法比使用Math.Pow和除法的方法快约35%。使用Math.Pow进行10,000,000次截断需要1.119秒,而使用这种方法只需要0.707秒。 - Propagating
显示剩余3条评论

41

使用模运算符:

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

结果:0.54


2
我不理解(读作:没有花时间验证)所有这些其他花哨的解决方案,这正是我在寻找的。谢谢! - Isaac Baker
1
在 .Net Fiddle 上运行此代码 点击 会产生 0.5400 的结果... 下面 D. Nesterov 的答案生成了预期的 0.54 - ttugates
1
你知道吗,@ttugates,0.54和0.5400是完全相同的值,对吧?除非/直到需要格式化显示时,后面跟着多少个零都无所谓——在这种情况下,如果正确格式化,则结果将是相同的:$"{0.54m:C}" 会产生 "$0.54",而 $"{0.5400m:C}" 也会产生 "$0.54" - Leonard Lewis
这是一个有点懒的回答,应该更新以实际显示将值返回到两个小数位,正如指出的那样,此代码并没有这样做。如果我们考虑完整性和严谨性,我是这样认为的,这只是一个部分答案,尽管它确实回答了去除舍入组件的问题,但在技术上并不完整。 - JoeTomks
我喜欢这个答案。简单但非常有效。 - princessbubbles15
1
感谢@LeonardLewis提供这个简单的答案。我只是稍微修改了一下,用Math.Round()将多余的0去掉了。 - AK3800

25

其他示例存在的一个问题是它们在除法之前将输入值乘以一个因子。这里存在一种边缘情况,即通过先进行乘法运算可能会导致十进制溢出,虽然这是一种较为特殊的情况,但我曾经遇到过。以下是一种更加安全的方法,可以将小数部分单独处理:

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

我知道这已经过时了,但我注意到了一个问题。你在这里使用的因子是整数类型,所以如果你要截断很多位小数(比如25位),它会导致最终结果出现精度误差。我通过将因子类型更改为十进制来解决了这个问题。 - TheKingDave
@TheKingDave:可能不相关,但是因子不能有小数点,所以将其建模为长整型会更好吧? - Ignacio Soler Garcia
@SoMoS 对我而言,十进制浮点数更适合,因为它给出了最高的因子存储值。虽然它仍有限制,但对于我的应用程序来说足够大了。相反,长整型无法为我的应用程序存储足够大的数字。例如,如果你使用长整型进行截断(Truncate)25,则会有一些不精确。 - TheKingDave
根据@TheKingDave的建议,更新允许截断更多位数的功能,感谢。 - Tim Lloyd

19

在 .NET Core 3.0 及其以后版本中,Math.RoundDecimal.Round 可以通过新的MidpointRounding.ToZero截断数字。对于正数,MidpointRounding.ToNegativeInfinity 具有相同的效果,而对于负数,相当于使用 MidpointRounding.ToPositiveInfinity

这些代码行:

Console.WriteLine(Math.Round(3.4679, 2,MidpointRounding.ToZero));
Console.WriteLine(Math.Round(3.9999, 2,MidpointRounding.ToZero));
Console.WriteLine(Math.Round(-3.4679, 2,MidpointRounding.ToZero));
Console.WriteLine(Math.Round(-3.9999, 2,MidpointRounding.ToZero));

产出:

3.46
3.99
-3.46
-3.99

根据文档,使用 ToZero 无需区分正负数:2.8 和 2.1 都会被截断为 2,对于 -2.8 和 -2.1 的结果也是 -2。 - Neph
重要提示:您需要使用.NET 5.0版本才能实现此功能,如果使用4.0版本,则只能使用"AwayFromZero"和"ToEven"。 - Neph
@Neph,没有“常规”的说法。.NET 5是.NET Core 5的当前版本。名称更改是出于营销目的。它包括几乎所有将要迁移到.NET Core的.NET旧API,因此决定通过统一名称来“统一”这些平台。 - Panagiotis Kanavos
@Neph 请查看Introducing .NET 5。第一行是“今天,我们宣布,在.NET Core 3.0之后的下一个版本将是.NET 5。这将是.NET家族中的下一个重大发布。”。这个决定在这个Github问题中有解释——无论使用什么名称,都会带来麻烦和混乱。我也不喜欢这个新命名。至少,将变化从“.NET Framework 4.8”变为“.NET 5.0”更容易向非技术经理推销。 - Panagiotis Kanavos
1
@Jaider 是的。我实际上链接到了枚举的文档。值为2的最后一个选项是“ToZero”。MidpointRounding页面中的所有示例都使用ToZero。你尝试在.NET Framework中使用它而不是.NET Core吗? - Panagiotis Kanavos
显示剩余7条评论

7
我将提供十进制数的解决方案。
这里一些十进制解决方案容易溢出(如果我们传递一个非常大的十进制数,方法将尝试将其乘以另一个数字)。
Tim Lloyd的解决方案能够防止溢出,但速度较慢。
以下解决方案约快2倍,且不会产生溢出问题:
public static class DecimalExtensions
{
    public static decimal TruncateEx(this decimal value, int decimalPlaces)
    {
        if (decimalPlaces < 0)
            throw new ArgumentException("decimalPlaces must be greater than or equal to 0.");

        var modifier = Convert.ToDecimal(0.5 / Math.Pow(10, decimalPlaces));
        return Math.Round(value >= 0 ? value - modifier : value + modifier, decimalPlaces);
    }
}

[Test]
public void FastDecimalTruncateTest()
{
    Assert.AreEqual(-1.12m, -1.129m. TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.120m. TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.125m. TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.1255m.TruncateEx(2));
    Assert.AreEqual(-1.12m, -1.1254m.TruncateEx(2));
    Assert.AreEqual(0m,      0.0001m.TruncateEx(3));
    Assert.AreEqual(0m,     -0.0001m.TruncateEx(3));
    Assert.AreEqual(0m,     -0.0000m.TruncateEx(3));
    Assert.AreEqual(0m,      0.0000m.TruncateEx(3));
    Assert.AreEqual(1.1m,    1.12m.  TruncateEx(1));
    Assert.AreEqual(1.1m,    1.15m.  TruncateEx(1));
    Assert.AreEqual(1.1m,    1.19m.  TruncateEx(1));
    Assert.AreEqual(1.1m,    1.111m. TruncateEx(1));
    Assert.AreEqual(1.1m,    1.199m. TruncateEx(1));
    Assert.AreEqual(1.2m,    1.2m.   TruncateEx(1));
    Assert.AreEqual(0.1m,    0.14m.  TruncateEx(1));
    Assert.AreEqual(0,      -0.05m.  TruncateEx(1));
    Assert.AreEqual(0,      -0.049m. TruncateEx(1));
    Assert.AreEqual(0,      -0.051m. TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.14m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.15m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.16m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.19m.  TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.199m. TruncateEx(1));
    Assert.AreEqual(-0.1m,  -0.101m. TruncateEx(1));
    Assert.AreEqual(0m,     -0.099m. TruncateEx(1));
    Assert.AreEqual(0m,     -0.001m. TruncateEx(1));
    Assert.AreEqual(1m,      1.99m.  TruncateEx(0));
    Assert.AreEqual(1m,      1.01m.  TruncateEx(0));
    Assert.AreEqual(-1m,    -1.99m.  TruncateEx(0));
    Assert.AreEqual(-1m,    -1.01m.  TruncateEx(0));
}

2
我不喜欢在它后面加上“Ex”。C#支持重载,您的“Truncate”方法将与.NET本机方法分组在一起,为用户提供无缝体验。 - Gqqnbig
1
你的算法结果有一些错误。默认的MidpointRounding模式是银行家舍入,它将0.5舍入到最近的偶数值。Assert.AreEqual(1.1m, 1.12m.TruncateEx(1));失败是因为这个原因。如果在Math.Round调用中指定“正常”舍入(AwayFromZero),那么Assert.AreEqual(0m, 0m.TruncateEx(1));也会失败。 - Jon Senchyna
1
此解决方案仅在您使用 MidpointRounding.AwayFromZero 并特别编写代码来处理值0时才能生效。 - Jon Senchyna
“Assert.AreEqual(1.1m, 1.12m.TruncateEx(1)); 失败” - 你说的“失败”是什么意思?这段代码已经在.NET 4、4.5.2和4.6.1上进行了测试。我在工作中和个人项目中都使用它,并且我的答案中的单元测试每天都会执行。而且测试通过了。我有遗漏什么吗?如果我的测试在你的机器上失败,请提供截图或异常详细信息。 - nightcoder
1
Jon 是正确的:0m.TruncateEx(0) 的结果为 -1,除非显式处理 0。同样,-11m.TruncateEx(0) 的结果为 -10,除非在 Math.Round 中使用 MidpointRounding.AwayFromZero。不过,在进行这些修改后似乎可以很好地工作。 - Ho Ho Ho
1
即使对AwayFromZero进行了更改并明确处理了0和-9999999999999999999999999999m.TruncateEx(0),结果仍然是-9999999999999999999999999998,因此在某些情况下仍然存在问题。 - Ho Ho Ho

6
这是一个老问题,但许多答案在处理大数时表现不佳或溢出。我认为D.Nesterov的回答是最好的:鲁棒、简单且快速。我只想再添一点。 我尝试了使用decimals并检查了源代码。来自public Decimal (int lo, int mid, int hi, bool isNegative, byte scale)构造函数文档

十进制数的二进制表示由1位符号、96位整数和一个缩放因子组成,该缩放因子用于除以整数并指定其中哪一部分是小数分数。缩放因子隐式地是10的指数,范围从0到28。

知道这一点后,我的第一种方法是创建另一个具有与我想要舍弃的小数相对应的比例的decimal,然后截断它,最后创建一个具有所需比例的decimal
private const int ScaleMask = 0x00FF0000;
    public static Decimal Truncate(decimal target, byte decimalPlaces)
    {
        var bits = Decimal.GetBits(target);
        var scale = (byte)((bits[3] & (ScaleMask)) >> 16);

        if (scale <= decimalPlaces)
            return target;

        var temporalDecimal = new Decimal(bits[0], bits[1], bits[2], target < 0, (byte)(scale - decimalPlaces));
        temporalDecimal = Math.Truncate(temporalDecimal);

        bits = Decimal.GetBits(temporalDecimal);
        return new Decimal(bits[0], bits[1], bits[2], target < 0, decimalPlaces);
    }

这种方法不比D. Nesterov的方法更快,而且更加复杂,因此我进行了一些尝试。我的猜测是需要创建一个辅助的decimal并两次检索位数使其变慢。在第二次尝试中,我自己操作了由Decimal.GetBits(Decimal d)方法返回的组件。思路是将组件除以10,直到需要的次数,并减小比例。代码基于(大量)参考Decimal.InternalRoundFromZero(ref Decimal d, int decimalCount)方法

private const Int32 MaxInt32Scale = 9;
private const int ScaleMask = 0x00FF0000;
    private const int SignMask = unchecked((int)0x80000000);
    // Fast access for 10^n where n is 0-9        
    private static UInt32[] Powers10 = new UInt32[] {
        1,
        10,
        100,
        1000,
        10000,
        100000,
        1000000,
        10000000,
        100000000,
        1000000000
    };

    public static Decimal Truncate(decimal target, byte decimalPlaces)
    {
        var bits = Decimal.GetBits(target);
        int lo = bits[0];
        int mid = bits[1];
        int hi = bits[2];
        int flags = bits[3];

        var scale = (byte)((flags & (ScaleMask)) >> 16);
        int scaleDifference = scale - decimalPlaces;
        if (scaleDifference <= 0)
            return target;

        // Divide the value by 10^scaleDifference
        UInt32 lastDivisor;
        do
        {
            Int32 diffChunk = (scaleDifference > MaxInt32Scale) ? MaxInt32Scale : scaleDifference;
            lastDivisor = Powers10[diffChunk];
            InternalDivRemUInt32(ref lo, ref mid, ref hi, lastDivisor);
            scaleDifference -= diffChunk;
        } while (scaleDifference > 0);


        return new Decimal(lo, mid, hi, (flags & SignMask)!=0, decimalPlaces);
    }
    private static UInt32 InternalDivRemUInt32(ref int lo, ref int mid, ref int hi, UInt32 divisor)
    {
        UInt32 remainder = 0;
        UInt64 n;
        if (hi != 0)
        {
            n = ((UInt32)hi);
            hi = (Int32)((UInt32)(n / divisor));
            remainder = (UInt32)(n % divisor);
        }
        if (mid != 0 || remainder != 0)
        {
            n = ((UInt64)remainder << 32) | (UInt32)mid;
            mid = (Int32)((UInt32)(n / divisor));
            remainder = (UInt32)(n % divisor);
        }
        if (lo != 0 || remainder != 0)
        {
            n = ((UInt64)remainder << 32) | (UInt32)lo;
            lo = (Int32)((UInt32)(n / divisor));
            remainder = (UInt32)(n % divisor);
        }
        return remainder;
    }

我没有进行严格的性能测试,但在MacOS Sierra 10.12.6、3.06 GHz英特尔Core i3处理器和针对.NetCore 2.1的情况下,这种方法似乎比D.Nesterov的方法快得多(由于我的测试不够严格,所以我不会给出具体数字)。由于增加了代码复杂度,实现此方法的人需要评估性能提升是否值得。


我必须点赞,因为你付出了很多思考和努力。你以Nesterov为基准,不断前进——向你致敬。 - AndrewBenjamin

3

你需要的是 ((long)(3.4679 * 100)) / 100.0 吗?


2
这对您有用吗?
Console.Write(((int)(3.4679999999*100))/100.0);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接