zoukankan      html  css  js  c++  java
  • C#中的四舍五入算法

    最近在产品开发过程中遇到一个问题,就是在对数值进行截取,例如说保留两位小数时,最终得到的结果跟预期的在某些情况下会产生差异,这个差异的表现就是最后一位与预期的不一致,也就是说在“四舍五入”上出现了问题。所以,专门抽时间看了一下。

    首先,我们需要确认一下舍入的规则,按照我们上小学的时候所学应该是“四舍五入”,也就是要保留的那一位之后的一位上的数字,如果是4就直接舍掉,如果是5,则在最后一位上加1。虽然简单,但还是举个例子。

    例1(保留2位小数):

    • 1.234 -> 1.23
    • 1.235 -> 1.24
    • -1.234 -> -1.23
    • -1.235 -> -1.23

    这种方法是我们所熟悉的,但是还需要向大家提供另外一种“舍入”算法,就是银行家舍入(Bankers rounding)算法。相比较我们所熟悉的四舍五入来说,这才是一种更国际通行的。事实上这也是 IEEE 规定的舍入标准。因此所有符合 IEEE 标准的语言都应该是采用这一算法的。其算法规则是“四舍六入五取偶”。详细点儿说,就是

    • 一个小数,当舍去位小于5,那么就舍去这位;
    • 当舍去位等于5的时候,那么去看舍去位前面一位数的奇偶性,如果是奇数,那么就舍去5,然后舍去位前面一位加1,相反:如果是偶数,那么就舍去5,舍去位保留偶数性质不变;
    • 当舍去位大于5的时候,那么舍去位不要,舍去位前面一位加1;
    • 这个法则对负数也起相同作用!

    也一样举个例子,例2(保留2位小数):

    • 1.234 -> 1.23
    • 1.235 -> 1.24
    • 1.236 -> 1.24
    • 1.245 -> 1.24
    • 1.255 -> 1.26

    如果大家还理解不了的话,可以找个小朋友问一下,据不可靠消息透露,现在他们会学这种算法的。

    背景知识普及完毕,下面说一下在.Net开发环境中的实际应用。因为刚刚也说过了,银行家舍入算法是IEEE 规定的舍入标准,所以在.Net中实际上默认的舍入算法也是这个。

    首先来重新认识一下Convert.ToInt32()方法

    public static int ToInt32(decimal value);
    public static int ToInt32(double value);
    public static int ToInt32(float value);
    

    这个方法是我们会经常用到的,不过在我执行下面的代码之前,从来没有想过,它还有这么个小坑。从执行结果可以很容易看出它遵循的就是银行家舍入算法。

    Console.WriteLine(Convert.ToInt32(12.5));   //12
    Console.WriteLine(Convert.ToInt32(12.51));  //13
    Console.WriteLine(Convert.ToInt32(13.5));   //14
    Console.WriteLine(Convert.ToInt32(14.5));   //14
    Console.WriteLine(Convert.ToInt32(15.5));   //16
    

    至于使用(int)num进行的强制类型转换,则是直接截断小数部分,相当于Math.Truncate()方法。

    啰嗦了好多了,下面是重点啦,对小数进行四舍五入。下面先列举一下平时开发过程中经常会用到的舍入方法:

    double num = 12.345;
    Console.WriteLine(num.ToString("F2"));	//12.35
    

    我个人觉得,ToString方法极为好用,既可以精确的进行四舍五入(没有找到明确的依据,但就目前测试的结果表现的确是这样的),还可以保留末尾的0,在最终界面输出时进行四舍五入,应该没有比这更好的方法了。如果对结果是字符串不满意,就再做一次Convert吧。

    public static decimal Round(decimal d, int decimals);
    public static double Round(double value, int digits);
    

    Math.Round()方法也是我们最常用的一种小数截取方法,但是不管我们意识到了没有,它们默认采用的都是银行家舍入法。所以,在我们不经意间,就有与我们预期不一致的数据悄悄的产生了。还好,Round方法提供了一个重载,使用一个MidpointRounding参数来决定舍入的算法:

    public static decimal Round(decimal d, int decimals, MidpointRounding mode);
    public static double Round(double value, int digits, MidpointRounding mode);
    
    public enum MidpointRounding
    {
        // Summary:
        //     When a number is halfway between two others, it is rounded toward the nearest even number.
        ToEven = 0,
        // Summary:
        //     When a number is halfway between two others, it is rounded toward the nearest number that is away from zero.
        AwayFromZero = 1,
    }
    

    当使用MidpointRounding.ToEven时,其实就是默认的银行家舍入法;而MidpointRounding.AwayFromZero,则是说当数值正好处于两侧的数字中间,也就是舍去位等于5,且其是最后一位时,返回距离0更远的那个数字。虽然这个描述有些麻烦,但其实就是我们的四舍五入。

    似乎问题已经解决了,调用Math.Round()时把MidpointRounding.AwayFromZero传进去就可以了,算几个数试试。

    decimal dn = 2.155m;
    Console.WriteLine(Math.Round(dn, 2, MidpointRounding.AwayFromZero));    //2.16
    dn = 4.155m;
    Console.WriteLine(Math.Round(dn, 2, MidpointRounding.AwayFromZero));    //4.16
    

    看着还是没什么问题,不过我们日常开发中decimal类型用的不是太多啊,更加常用的是浮点数,那就再用浮点数试一下。

    double dn = 2.155;
    Console.WriteLine(Math.Round(dn, 2, MidpointRounding.AwayFromZero));    //2.15
    dn = 4.155;
    Console.WriteLine(Math.Round(dn, 2, MidpointRounding.AwayFromZero));    //4.16
    

    似乎跟预期的不大一样啊,原因嘛,看到问题就很容易想得到了,万恶浮点数啊,人家就是不精确,不能用==,不能说你看到它显示成2.155,他就绝对是2.155,所以就会偶尔产生这样跟我们预期不一样的结果,解决方法也各种各样,可以先转换成decimal类型再做处理,也可以按前面说的ToString之后再做类型转换,更可以自己来实现一个四舍五入的算法。下面提供两个算法实现,其原理都是先加5,再截取,方法是提供了,但还是要先验证啊:

    private static double ChineseRound(double dblnum, int numberprecision)
    {
        int tmpNum = dblnum > 0 ? 5 : -5;
        return Math.Truncate((Math.Truncate(dblnum * Math.Pow(10, numberprecision + 1)) + tmpNum) / 10) / Math.Pow(10, numberprecision);
    }
    
    private static double ChineseRound2(object objnum, int numberprecision)
    {
        double returnnum = 0;
        if (objnum != null)
        {
            try
            {
                double dblnum = double.Parse(objnum.ToString());
                int tmpNum = dblnum > 0 ? 5 : -5;
                double dblreturn = Math.Truncate(dblnum * Math.Pow(10, numberprecision + 1)) + tmpNum;
    
                dblreturn = Math.Truncate(dblreturn / 10) / Math.Pow(10, numberprecision);
                returnnum = dblreturn;
            }
            catch { }
        }
        return returnnum;
    }
    

    最后写个方法测试一下上面提到的几种舍入的算法:

    	static void Main(string[] args)
        {
    
            var d = 2.155d;
            var step = 0.01d;
            var precision = 2;
            
            for (var i = 0; i < 10; i++, d += step)
            {
                Console.WriteLine("{0}	{1}	{2}	{3}	{4}	{5}	{6}	"
                    , d
                    , ChineseRound(d, precision)
                    , ChineseRound2(d, precision)
                    , Convert.ToDouble(d.ToString("F" + precision))
                    , Math.Round(d, precision)
                    , Math.Round(d, precision, MidpointRounding.AwayFromZero)
                    , Math.Round((decimal)d, precision, MidpointRounding.AwayFromZero));
            }
    
            d = -d;
            for (var i = 0; i < 10; i++, d -= step)
            {
                Console.WriteLine("{0}	{1}	{2}	{3}	{4}	{5}	{6}	"
                    , d
                    , ChineseRound(d, precision)
                    , ChineseRound2(d, precision)
                    , Convert.ToDouble(d.ToString("F" + precision))
                    , Math.Round(d, precision)
                    , Math.Round(d, precision, MidpointRounding.AwayFromZero)
                    , Math.Round((decimal)d, precision, MidpointRounding.AwayFromZero));
            }
            Console.ReadKey();
        }
    

    结果大家就自己执行一下看看吧,看看能发现什么问题。

    PS.1 说一下最后那段代码执行后得到的结论,ChineseRound这个方法还是逃不开浮点数的坑,而ChineseRound2在测试过程中还没发现错误,这是让人挺费解的一个地方。所以对ChineseRound方法做了一点儿小修改,结果就对了(至少针对目前的测试而言)

    private static double ChineseRound(double src, int precision)
    {
    	src = Convert.ToDouble(src.ToString());		//添加了这么一行代码
    	int tmpNum = src > 0 ? 5 : -5;
    	return Math.Truncate((Math.Truncate(src * Math.Pow(10, precision + 1)) + tmpNum) / 10) / Math.Pow(10, precision);
    }
    

    PS.2 以前好像是没写过这样做分享的文章,就是一点儿东西,但是写得有些啰嗦,也不晓得能不能表述清楚,欢迎大家提意见啊
    PS.3 写作过程中,参考了一些互联网上的文章,包括但不限于ChineseRound2方法的实现

    http://bigman.pw
  • 相关阅读:
    C陷阱与缺陷代码分析之第2章语法陷阱
    Linux tail命令
    spring利用扫描方式对bean的处理(对任何版本如何获取xml配置信息的处理)
    mysql 初识之日志文件篇
    JavaScript实现复制功能
    [置顶] Hibernate从入门到精通(七)多对一单向关联映射
    android操作通讯录的联系人
    数据结构读书笔记(三)(C语言)
    Nginx 日志分析
    [WARNING] Using platform encoding (GBK actually) to copy filtered resources, i.e. build is platform
  • 原文地址:https://www.cnblogs.com/renzhiwei/p/4229384.html
Copyright © 2011-2022 走看看