2024年11月

〇、js 的数值计算存在结果不精确的情况

最近接触财务相关系统,页面上会有一些简单的计算,就发现其实是非常简单的计算,但 js 计算出来的结果却不是预期值,可能带上一大串 0 或 9,导致计算结果错误,本文来简单汇总下,以及如何处理来避免这个问题。

先看看都会有哪些不精确的情况。

// 【加减乘除 四种基础运算】
// 精度问题其实也不是全都会有,比如以下测试:
let num1 = 0.1;
let num2 = 0.1;
let result = num1 + num2;
console.log("0.1+0.1 = " + result);
// 0.1+0.1 = 0.2
// 0.1+0.2 = 0.30000000000000004
// 1.1*1.2 = 1.32
// 1.1*1.1 = 1.2100000000000002
// 其实四种基础运算都会出现类似情况,这里就简单列两类

// 【数值比较】
console.log("'0.1+0.2 = 0.3' 比较结果:", 0.1 + 0.2==0.3);
// '0.1+0.2 = 0.3' 比较结果: false

一、原因简介

因为计算机内部的信息都是由二进制方式表示的,即 0 和 1 组成的各种编码,但由于
某些浮点数
没办法用二进制准确的表示出来,也就带来了一系列精度问题。当然这也不是 js 独有的问题。

推荐一个文章吧,有兴趣可以深究下:
https://zhuanlan.zhihu.com/p/33333351
,讲的应该是很详细,但有些复杂博主看了一遍也不是太了解。

下面把大概的流程梳理下。

  • 将小数转成二进制数

小数部分乘 2 取整数部分,若小数不为 0 则继续乘 2,直至小数部分为 0。然后将取出的整数位正序排列。整数部分就除以 2。

0.1 * 2 = 0.2   // 取0
0.2 * 2 = 0.4   // 取0
0.4 * 2 = 0.8   // 取0
0.8 * 2 = 1.6   // 取1
0.6 * 2 = 1.2   // 取1
0.2 * 2 = 0.4   // 取0
......

0.1 的二进制表示是:0.000110011......0011.....(0011无限循环)
0.2 的二进制表示是:0.00110011......0011......(0011无限循环)

虽然是无限长度,但是还是得有一个标准的显示方式,那就是 IEEE 754 标准。

  • IEEE 754 标准方法表示数值

IEEE 754 标准是 IEEE 二进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号。IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换、算术格式以及方法。

详细的标识法,就跳过了,直接来看结果。

js 只有一种数字类型 number,而 number 使用的是
IEEE 754 双精度浮点格式
。最高位是一位符号位(0正 1负),后面的 11 位是指数,剩下的 52 位为尾数(有效数字)。

// js 中 0.1 的二进制存储格式为:
// 符号位用逗号分隔,指数位用分号分隔
0.1:0,01111111011;1001100110011001100110011001100110011001100110011010
0.2:0,01111111100;1001100110011001100110011001100110011001100110011010
  • 计算两个二进制数的和
// 二进制形式的结果
sum = 0.010011001100110011001100110011001100110011001100110100
// 最后再转成十进制
sum = 2^2 + 2^5 + 2^6 + ... + 2^52 = 0.30000000000000004440892098500626
// 近似后的结果:0.30000000000000004

详情可参考: https://zhuanlan.zhihu.com/p/33333351

二、解决方式

2.1 Math.Round() 函数,先乘后除 10 的 n 次方

根据需求的精度,
先乘以
10 的 n 次方,通过 Math.Round() 函数
取整
后,
再除以
10 的 n 次方。

function numToString(num){
    let factor = Math.pow(10, 4); // 最多保留 4 位小数
    // 通过乘以一个因子(例如 10 的 4 次方),然后四舍五入
    // 最后,再除以相同的因子,可以实现对特定小数位数的精确控制
    let roundedNum = Math.round(num * factor) / factor;
    return roundedNum.toString();
}

关于 Math.Round() 四舍五入的规则,可以参考以下测试结果:

console.log("Math.round(4.2)     ", Math.round(4.2)  );
console.log("Math.round(4.5)     ", Math.round(4.5)  );
console.log("Math.round(4.7)     ", Math.round(4.7)  );
console.log("Math.round(-4.2)    ", Math.round(-4.2) );
console.log("Math.round(-4.5)    ", Math.round(-4.5) );
console.log("Math.round(-4.7)    ", Math.round(-4.7) );
console.log("Math.round(1.5)     ", Math.round(1.5)  );
console.log("Math.round(2.5)     ", Math.round(2.5)  );
console.log("Math.round(3.5)     ", Math.round(3.5)  );
console.log("Math.round(-1.5)    ", Math.round(-1.5) );
console.log("Math.round(-2.5)    ", Math.round(-2.5) );
console.log("Math.round(-3.5)    ", Math.round(-3.5) );

可以看出,
正数的小数位为 5 时,进 1;负数小数位为 5 时,舍弃

2.2 toFixed() 方法,直接取小数点后固定位数

此方法就是直接
指定要保留的几位小数,若小数位较少,就会以 0 补全

toFixed() 的四舍五入规则,并非严格的根据要保留小数位后边的小数来判断。

若作为判断的小数位为 5,且后边没有大于 0 的数,则舍入到最近的奇数;若 5 后边有非零的值,就直接进 1。

例如,4.55 保留 1 位小数,就是 4.5,但 4.5500001 的结果就是 4.6。

例如,-4.55 保留 1 位小数,就是 -4.5,但 -4.5500001 的结果就是 -4.6。

如下示例,供参考:

let num = 4.22;
console.log("num.toFixed(1):4.22)     ", num.toFixed(1));
num = 4.55;
console.log("num.toFixed(1):4.55)     ", num.toFixed(1));
num = 4.551;
console.log("num.toFixed(1):4.551)    ", num.toFixed(1));
num = 4.65;
console.log("num.toFixed(1):4.65)     ", num.toFixed(1));
num = 4.77;
console.log("num.toFixed(1):4.77)     ", num.toFixed(1));
num = -4.22;
console.log("num.toFixed(1):-4.22)    ", num.toFixed(1));
num = -4.55;
console.log("num.toFixed(1):-4.55)    ", num.toFixed(1));
num = -4.551;
console.log("num.toFixed(1):-4.551)   ", num.toFixed(1));
num = -4.65;
console.log("num.toFixed(1):-4.65)    ", num.toFixed(1));
num = -4.77;
console.log("num.toFixed(1):-4.77)    ", num.toFixed(1));
num = -4.77;
console.log("num.toFixed(1):-4.77)    ", num.toFixed(4));

注意:
toFixed() 的结果是字符串类型
,若最终还需要 number 类型,就需要通过 Number() 函数进行转换。

2.3 通过正则表达式,多余位小数直接舍去

必须先将数字类型转换成字符串,再使用 match() 方法。

let num = 3.14959;
let numStr = num.toString(); // 必须为字符串
let fixedNumStr = numStr.match(/^-?\d+(\.\d{0,2})?/)[0]; // 两位小数的正则表达式
let fixedNum = parseFloat(fixedNumStr);
console.log(fixedNum); // 输出:3.14

热点随笔:

·
【故障公告】k8s集群2台32核64G节点服务器被释放造成全站故障
(
博客园团队
)
·
强!34.1K star! 再见Postman,新一代API测试利器,功能强大、颜值爆表!
(
狂师
)
·
.NET开发者福音:JetBrains官方宣布 Rider 非商用免费开放!
(
追逐时光者
)
·
又给会员送福利,100台一年华为云2核2G3M云服务器
(
博客园团队
)
·
.NET 全能高效的 CMS 内容管理系统
(
小码编匠
)
·
.NET 8 高性能跨平台图像处理库 ImageSharp
(
小码编匠
)
·
解决DDD最大难题-如何划分领域
(
老肖想当外语大佬
)
·
.NET 高性能异步套接字库,支持多协议、跨平台、高并发
(
小码编匠
)
·
部署个SSH蜜罐玩玩,既能增强安全性,又能当电子蛐蛐
(
程序设计实验室
)
·
Avalonia开源控件库强力推荐-Semi.Avalonia
(
沙漠尽头的狼-Dotnet9
)
·
带团队后的日常思考(十六)
(
咖啡机(K.F.J)
)
·
我的博客网站为什么又回归Blazor了
(
沙漠尽头的狼-Dotnet9
)

热点新闻:

·
“边吃火锅边唱歌”,国产新能源车找到了人性的弱点
·
3个思想实验撕裂时空!实验证实:人类居住时空并非物理实体,而是近似
·
最高精度1公里*1公里*1小时!达摩院发布气象大模型,落地大山东
·
图集|知道吗?神舟系列航天员们出差太空累计1065天啦
·
“25万买车,12万换电池!”天价维修费揭秘
·
华为Mate 70系列来了!余承东宣布:史上最强大的Mate 11月见
·
成功着陆!返回地球后第一顿饭,3名航天员都点了这道餐食
·
小红书将业界最大数据湖迁至阿里云
·
扎克伯格逃过一劫
·
比亚迪被曝又一“大动作”
·
因贪腐,网易游戏一高管被带走调查
·
“垃圾外卖”背后推手:批量复制门店,加盟狂揽2000万

最近和重点行业的用户交流技术方案,闲聊时用户反馈:
信息化项目全部失败,供应商都换了一批了

甲方能说出来这样的实话反应了背后多少心酸和无奈,花着金钱和耗费时间,只是证明自己错了,甚至大家都错误了。

我只是轻描淡写的回了一句:
现在甲方找乙方确实不好选择,供应商水平也是参差不齐
。换位思考,我一直持这样的观点,当然也侧面证明我们做项目还有初心、还有良心。

到底是为什么在一个用户这里出现这样普遍现象了?做过项目的同志们能给出来N条原因,例如:项目价格低、需求不明确、项目执行管理不到位、人员水平差等等。

至少,从我接触用户的感觉来讲,他们是比较好的用户。有项目任务书和需求文档、交流也没有障碍、不是最低价中标、回款也不错。在我的认知里是一个比较优质客户。

总之,一个项目的失败有各种原因,大家都会从各自的角度找原因,很难找到一个主要原因。和一个公司的研发负责人闲聊,他反馈他们公司的传统优势领域的项目做的还可以,但是其他开拓的领域做的比较差,他分析原因是:

1
)公司产品不行,就没有开发产品的基因;(2
)项目人员波动比较大;(3
)新接替项目的人员,看以前项目做的啥也不是,总想推倒重来
。这是乙方公司的自我分析项目失败的原因,说的比较实在。

能不能从甲乙双方找到一条项目失败的共同原因
?首先我认为和技术、管理、费用都没有太大关系。非要找到一条项目失败的共同原因,我认为:
甲乙双方从上到下都在玩投机的游戏(公式:甲投机
||
乙投机=
项目失败)
。这个只能是大家悟了,不过多解释了。

前年跟踪了一个2000万预算的项目,跟踪了15个月,最终让其他公司以最低价1100多万拿走了。前段时间出差,区域领导反馈该公司做的项目也不怎么样。

可能这个世界就是草台班子组成的,大家也不必太认真了。


物联网&大数据技术 QQ群:54256083

物联网&大数据项目 QQ群:727664080

QQ:504547114

微信:wxzz0151

博客:https://www.cnblogs.com/lsjwq

微信公众号:iNeuOS

推荐一个跨平台、模块化、可扩展且超快速的开源一体化电子商务平台。


01
项目简介

Smartstore 支持桌面和移动平台、多语言、多商店、多货币的商城,并支持SEO优化,支持无限数量的产品和类别、报表、ESD、折扣、优惠券等等。

还有一套全面的 CRM 和 CMS、销售、营销、付款和物流处理等工具可以满足所有需求的强大一体化解决方案。


02
技术架构

1、采用当前流行的ASP.NET Core 7 + Entity Framework Core 7 领域驱动设计架构,方便扩展;

2、跨平台:支持在 Windows、Linux 或 Mac 上运行;

3、支持Docker开箱即用,轻松部署;

4、支持模块化设计,可组合、可扩展且高度灵活;

5、强大的主题引擎,允许你通过主题创建或自定义主题和皮肤;

6、Liquid 模板引擎:高度灵活的电子邮件和营销活动模板;

7、高度可扩展,具有页面缓存和集群支持;

8、简洁的用户界面,采用Vue.js、Bootstrap、Sass开发;

9、支持数据库:Microsoft SQL Server、MySQL、PostgreSQL 11、 SQLite。


03
项目结构

图片


04
部分界面截图

前端首页
:PC界面

图片

前端首页
:移动界面

图片

后台管理

图片

类别

图片

订单

图片

会员

图片

优惠

图片

CMS

图片

分类

图片


05项目地址

https://github.com/smartstore/Smartstore

更多开源项目请查看:
一个专注推荐优秀.Net开源项目的榜单

- End -

推荐阅读

一个.Net Core开源监控解决方案,支持Redis、Elasticsearch、SqlServer

C#与.Net、.Net Core有什么关系?

一个.Net Core开源缓存中间件,让你更加简单、方便使用缓存

基于.NET 7 + iView 的前后端分离的通用后台管理系统开源框架

一个C#开发的开源的快速启动工具

以前做移动端项目的时候都是用rem来做适配,现在基本上都是通过viewport单位来做。
postcss-px-to-viewport
就是一个将px单位转换为视口单位的 (vw, vh, vmin, vmax) 的 PostCSS 插件,它可以将你CSS中的px单位转化为vw,1vw等于1/100视口宽度。

1.安装

javascript

代码解读
复制代码
$ npm install postcss-px-to-viewport --save-dev

2.配置参数

在项目根目录创建
postcss.config.js
文件,添加如下配置。

javascript

代码解读
复制代码
module.exports = ({ webpack }) => {
  return {
    plugins: {
      autoprefixer: {},
      "postcss-px-to-viewport": {
        unitToConvert: "px", // 要转化的单位
        viewportWidth: 750, // UI设计稿的宽度
        unitPrecision: 6, // 转换后的精度,即小数点位数
        propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
        viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
        fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
        selectorBlackList: [], // 指定不转换为视窗单位的类名,
        minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
        mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
        // replace: true, // 是否转换后直接更换属性值
        exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
        landscape: false // 是否处理横屏情况
      }
    }
  }
}

启动项目,此时已经自动进行了转换,会根据
postcss.config.js
文件中的
viewportWidth
的值将
px
转换为
vw
,比如说设置div宽度为750px,转换后就是100vw。此时的样式如下:

输入

css

代码解读
复制代码
.class {
  margin: -10px .5vh;
  padding: 5vmin 9.5px 1px;
  border: 3px solid black;
  border-bottom-width: 1px;
  font-size: 14px;
  line-height: 20px;
}

输出

css

代码解读
复制代码
.class {
  margin: -3.125vw .5vh;
  padding: 5vmin 2.96875vw 1px;
  border: 0.9375vw solid black;
  border-bottom-width: 1px;
  font-size: 4.375vw;
  line-height: 6.25vw;
}

3.vant

(1).问题1 vant中组件的css单位没有转换

将所有的配置好之后,启动项目,添加了vant的组件后,打开检查,发现仍然是px单位,没有进行转换。

因为
postcss.config.js
文件中的
exclude
参数将整个
node_modules
文件夹中的组件都给排除掉了,安装的插件会使用它默认的单位,将
exclude
改为
[]

(2).问题2 转换后的vant组件特别小

此时vant组件的单位也进行了转换,但是展示会特别的小。

通过排查,在github上找到vant的官方demo,发现vant设置视口宽度是375,而我们设置的宽度为750,所以vant组件在转换之后会宽高都变为原来的一半大小。

所以我们要设置当转换到vant组件样式的单位时,将视口宽度设置为375,通过百度(cv工程师),将
postcss.config.js
配置修改如下:

javascript

代码解读
复制代码
const path = require("path")
module.exports = ({ webpack }) => {
  const designWidth = webpack.resourcePath.includes(path.join("node_modules", "vant")) ? 375 : 750

  return {
    plugins: {
      autoprefixer: {},
      "postcss-px-to-viewport": {
        unitToConvert: "px", // 要转化的单位
        viewportWidth: designWidth,
        unitPrecision: 6, // 转换后的精度,即小数点位数
        propList: ["*"], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
        viewportUnit: "vw", // 指定需要转换成的视窗单位,默认vw
        fontViewportUnit: "vw", // 指定字体需要转换成的视窗单位,默认vw
        selectorBlackList: [], // 指定不转换为视窗单位的类名,
        minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
        mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
        // replace: true, // 是否转换后直接更换属性值
        exclude: [], // 设置忽略文件,用正则做目录名匹配 /node_modules/
        landscape: false // 是否处理横屏情况
      }
    }
  }
}

此时页面已经恢复了正常。

4.行内样式

后续在使用vant的image组件的时候发现,在传入width和height
750
之后,图片超出了页面,打开检查发现仍然是px单位,没有转化为vw,传入的width和height是加到了img标签父元素的行内样式上,创建一个div实验了一下,
postcss-px-to-viewport
不会对行内样式进行转换,只会转换卸载style标签中的样式。给image组件添加了class,展示正常。

使用width和height传入:

使用class控制样式:

至此,结束