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}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t"
, 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}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t"
, 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方法的实现
C#中的四舍五入算法的更多相关文章
- opencv3中的机器学习算法之:EM算法
不同于其它的机器学习模型,EM算法是一种非监督的学习算法,它的输入数据事先不需要进行标注.相反,该算法从给定的样本集中,能计算出高斯混和参数的最大似然估计.也能得到每个样本对应的标注值,类似于kmea ...
- Delphi中的四舍五入函数
一.Delphi中的四舍五入法 四舍五入是一种应用非常广泛的近似计算方法,针对不同的应用需求,其有算术舍入法和银行家舍入法两种. 所谓算术舍入法,就是我们通常意义上的四舍五入法.其规则 ...
- Java中的经典算法之冒泡排序(Bubble Sort)
Java中的经典算法之冒泡排序(Bubble Sort) 神话丿小王子的博客主页 原理:比较两个相邻的元素,将值大的元素交换至右端. 思路:依次比较相邻的两个数,将小数放在前面,大数放在后面.即在第一 ...
- 分布式数据库中的Paxos 算法
分布式数据库中的Paxos 算法 http://baike.baidu.com/link?url=ChmfvtXRZQl7X1VmRU6ypsmZ4b4MbQX1pelw_VenRLnFpq7rMvY ...
- Java中的查找算法之顺序查找(Sequential Search)
Java中的查找算法之顺序查找(Sequential Search) 神话丿小王子的博客主页 a) 原理:顺序查找就是按顺序从头到尾依次往下查找,找到数据,则提前结束查找,找不到便一直查找下去,直到数 ...
- Java中的经典算法之选择排序(SelectionSort)
Java中的经典算法之选择排序(SelectionSort) 神话丿小王子的博客主页 a) 原理:每一趟从待排序的记录中选出最小的元素,顺序放在已排好序的序列最后,直到全部记录排序完毕.也就是:每一趟 ...
- STL中的查找算法
STL中有很多算法,这些算法可以用到一个或多个STL容器(因为STL的一个设计思想是将算法和容器进行分离),也可以用到非容器序列比如数组中.众多算法中,查找算法是应用最为普遍的一类. 单个元素查找 1 ...
- 在opencv3中的机器学习算法
在opencv3.0中,提供了一个ml.cpp的文件,这里面全是机器学习的算法,共提供了这么几种: 1.正态贝叶斯:normal Bayessian classifier 我已在另外一篇博文中介 ...
- Java中的排序算法(2)
Java中的排序算法(2) * 快速排序 * 快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists). * 步骤为: * 1. 从数 ...
随机推荐
- ActionLink 的一些小问题
近日为群友解答问题时遇到一个问题 由于自己以前确实没碰到过 特此记录一下 起因是群友想要用htmlhelper实现这样一个效果 <a href="我是链接" class=&q ...
- (转)apache的keepalive和keepalivetimeout(apache优化)
KeepAlive指的是保持连接活跃,类似于Mysql的永久连接. 如果将KeepAlive设置为On,那么来自同一客户端的请求就不需要再一次连接,避免每次请求都要新建一个连接而加重服务器的负担. ...
- ios应用,今年最蛋疼的6月,IPV6!!
刚刚苹果大会结束,你是不是后悔没去听他的发布会,!!有钱么你? iPV6 国人蒙蔽了,介是什么鬼,经过两三次的残忍拒绝,我认真去研究了iPV6, 2.2 Details We discovered ...
- 个人博客设计:创建Sql数据库操作类。
整体的博客框架如下 数据库操作java类如下 package com.yxq.dao; import java.sql.Connection; import java.sql.DriverManage ...
- UI层调用WCF服务实例(源码)
WCF原理性的东西,暂时还没有深入研究,只是在公司的项目中使用到了,会调用,然后再多做了一些了解,现在将它抽出来了一个小实例,写了一个WCF的demo. 我写的这个WCF.Demo主要包括数据契约和服 ...
- dx环境搭建
我使用的是vs2012+DXSDK_Jun10 DXSDK_Jun10下载地址http://download.microsoft.com/download/A/E/7/AE743F1F-632B-48 ...
- Binary Tree Inorder Traversal 解题思路 ×
问题: 非递归中序遍历二叉树 思路: 1.大循环,判断节点是否为空,栈是否为空 2.不为空:点进栈,向左走 3.为空:为空,出栈,读取值,向右走
- php解决下单、抽奖并发导致的库存负数的问题
我们知道数据库处理sql是一条条处理的,假设购买商品的流程是这样的: sql1:查询商品库存 if(库存数量 > 0) { //生成订单... sql2:库存-1 } 当没有并发 ...
- async: false的应用.
目的: 手机webview中, js ajax请求, success后, 进行window.open 操作 问题: 在Android, IOS均不能执行window.open 解决办法: 设置ajax ...
- CentOS 6.4 64位 安装 apache-tomcat-6.0.43
下载 tomcat: 地址:http://mirrors.hust.edu.cn/apache/tomcat/tomcat-6/v6.0.43/bin/apache-tomcat-6.0.43.tar ...