nstr - number → string, but looks good - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
Livid
180.99D
591.61D
V2EX    Javascript

nstr - number → string, but looks good

  •  2
     
  •   Livid
    PRO
    114 天前 5491 次点击
    这是一个创建于 114 天前的主题,其中的信息可能已经有所发展或是发生改变。

    https://nstr.vercel.app/

    一个处理数字显示问题的 JS 库。

    尤其是在处理有小数点的数字时会很有用。

    32 条回复    2025-08-27 09:44:35 +08:00
    terryso
        1
    terryso  
       114 天前
    好东西, 看看去
    MENGKE
        2
    MENGKE  
       114 天前
    很有用,感谢分享
    pyyalt
        3
    pyyalt  
       114 天前
    好用,支持!
    Ketteiron
        4
    Ketteiron  
       114 天前
    确实 looks good, 源码才 113 行,实现方法很巧妙。

    console.log(new Decimal('0.1').plus(new Decimal('0.2')).toString())
    vs
    console.log(nstr(0.1+0.2))

    因为设计使然,超过 4 位(可自行设定)的 9 和 0 会被认为是噪音然后截断处理,不适用多位小数严格计算场景,其他场景都不错。
    cvooc
        5
    cvooc  
       114 天前
    另外如果有严格小数计算需求, 可以考虑 bignumber.js ,精度极高无任何误差, 在我以前某无聊的变态超大数计算项目里验证过, 小数点后精度 9999 无压力
    Valid
        6
    Valid  
       114 天前
    好东西
    mrlmh00
        7
    mrlmh00  
       114 天前
    我看样例都看不懂
    显示不了 451.79 ?
    451.79999999456789 = 451.8
    451.78999999456789 = 451.78
    451.77999999456789 = 451.78
    451.76999999456789 = 451.77
    hamsterbase
        8
    hamsterbase  
       114 天前   3
    不推荐这个库

    推荐使用 decimal.js 实现精确的计算。
    而不是计算以后猜数字


    0.3 - 0.1 // 0.19999999999999998
    x = new Decimal(0.3)
    x.minus(0.1) // '0.2'
    x // '0.3'
    Ketteiron
        9
    Ketteiron  
       114 天前
    @mrlmh00 #7
    这个库只是用来处理 0.1+0.2=0.30000000000000004 这种问题的,让简单的浮点计算适合人类阅读
    产生这种问题的浮点数都有个共同的特点:一连串的 0 或者 9 ,所以这个库就检测 0/9 连续出现的次数,超过判断条件后截断全部 0/9 最后简单处理一下进位
    需要多位小数严谨计算的应该用 decimal.js, bignumber.js
    这个库我觉得已经说得很清楚了,nstr ,就是数字转字符串,字符串是用来看的不是计算的,不需要严谨小数点计算的前端纯展示场景非常非常多
    FlashEcho
        10
    FlashEcho  
       114 天前
    为啥不用成熟的高精度库呢,这个库的规则太简单粗暴了
    Ketteiron
        11
    Ketteiron  
       114 天前   1
    @chesha1 #10 我觉得我在 4#的例子已经很有说服力了,在你想展示几个浮点数的计算结果时,你希望写的是
    new Decimal(a).plus(new Decimal(b).times(new Decimal(c))).minus(new Decimal(d))
    还是
    nstr(a+b*c-d)
    Mint0315
        12
    Mint0315  
       114 天前
    有用!
    mrlmh00
        13
    mrlmh00  
       114 天前
    @dssxzuxc 没看懂啊 451.78999999456789 = 451.78 为什么没有进位。。
    Ketteiron
        14
    Ketteiron  
       114 天前
    @mrlmh00 #13 研究了一下
    https://github.com/shuding/nstr/blob/main/src/index.ts#L64-L77
    进位失败是因为
    451.78+0.01 = 451.78999999999996
    而不是期望的 451.79
    需要一个更稳健的进位方法来解决这个问题
    FlashEcho
        15
    FlashEcho  
       114 天前
    @dssxzuxc #11 我会在后端做好 let x = new Decimal(a).plus(new Decimal(b).times(new Decimal(c))).minus(new Decimal(d)),然后让前端 {x}
    UnluckyNinja
        16
    UnluckyNinja  
       114 天前
    说计算精度需求的都跑偏了,网站里面的示例很清楚了,就是为了解决数字格式化的问题,避免因为精度误差导致显示的数字过长或过早使用科学计数法.

    不过 100 多行还是有点多了,网站的对比看下来,toFixed 其实很符合要求,只是不会移除尾部 0 ,那么其实再替换下尾部 0 就够了,一行解决
    200.0003.toFixed(3).replace(/(\.?|(?<=\.\d+))0+$/,'')
    (?<=\.\d+) 就是确保处于小数部分,避免移除了整数部分的尾部 0 ,可能还有其它边界情况没考虑,差不多这个意思。
    Chuckle
        17
    Chuckle  
       114 天前
    适合数据可视化上数字展示
    zbinlin
        18
    zbinlin  
       113 天前
    @dssxzuxc #14 我去提了个 PR
    Ketteiron
        19
    Ketteiron  
       113 天前   1
    @UnluckyNinja #16
    我以前项目也写过类似的东西
    ```ts
    function n_str(
    value: number,
    options: { fractionDigits?: number } = {},
    ): string {
    const { fractiOnDigits= 3 } = options
    const str = value.toFixed(fractionDigits).replace(/(\.?|(?<=\.\d+))0+$/, '')
    return str === '-0' ? '0' : str
    }
    ```
    toFixed()的问题是无法智能判断精度,需要传入参数指定精度
    nstr(1.123456) -> 1.123456
    n_str(1.123456) -> 1.123
    n_str(1.123456, { fractionDigits: 6 }) -> 1.123456
    这里的难点是无法简单判断出浮点数计算的小数位数,比如 0.1+0.2 ,人类都知道应该是 1 位小数点,但是如何从 0.30000000000000004 解析出来,还有科学计数法把这个问题搞得更加复杂,而 nstr 能解决这个问题。手动指定精度 2 行就解决了,为了少写这个参数我不介意在项目里多 install 一个包。

    目前 nstr 我测了所有情况,就剩 456.78999999456789=456.78 这个问题,其他的都正常
    在 nstr 的方案上使用 toFixed 或许能解决这个问题同时不引入其他边缘情况,例如楼上的 pr
    https://github.com/shuding/nstr/pull/2/commits/4713d6447bc8fd3af2e246e63ea2f0edb8445d07
    bli22ard
        20
    bli22ard  
       112 天前
    这个库设计很有问题,0.1+0.2 已经为浮点数了,然后又转字符串,还能将 0.1+0.2 还原为 0.3 。如果你想表示 0.33999999999999998 ,它会给你转 0.34 ,结果就是 0.33999999999999998==0.34 这么做,明显有问题
    Ketteiron
        21
    Ketteiron  
       112 天前
    @bli22ard #20
    0.3399999999999999 = 0.34 是预期行为,这个库就是用来做这件事的。
    这已经是第四遍回答了,当你需要显示/计算标准浮点/decimal 时,请使用原生 js 或者 decimal.js, bignumber.js 。
    #16 已经说的很清楚了,这个库的作用是
    `解决数字格式化的问题,避免因为精度误差导致显示的数字过长或过早使用科学计数法`。
    设计上超过一定数量的连续 0 or 9 会直接截断,很显然这样才能实现 0.1 + 0.2 = 0.3 ,而带来的代价就是无法表达含有超过一定数量的连续 0 or 9 。
    这个库的适用场景应该是大屏、统计页面等等各种数据可视化,四位以内小数运算是正确的,连续 0 or 9 超过一定次数就会自动四舍五入,严格计算老实去用 decimal.js, bignumber.js ,这只是解决了非严格 decimal 数据的展示问题,让编写显示浮点计算结果的代码更简单易懂适合人类阅读。
    bli22ard
        22
    bli22ard  
       112 天前
    @dssxzuxc #21 可能做法简洁,但是,我不认为是一个好方法,这样会让行为变得模糊起来。小数最佳实战应该是 decimal.js, bignumber.js 等计算完,然后格式化输出,而不是用这个格式化,造成 0.33999999999999998 看起来像 0.34 的不精确问题
    UnluckyNinja
        23
    UnluckyNinja  
       112 天前 via Android
    @bli22ard #22 怎么还没绕过这个弯,就不是精度的问题。会取近似值的,epsilon 都远远大于精度误差,根本没必要上高精度库,高精度库也解决不了显示问题。
    就假设我有一个原始就是 0.3000004 的数(不是通过计算得来的,可能是网络,可能是可视化的数据集,总之这个数字本身就是这个形式),不需要计算,直接显示成人类可读字符串表示,这个情况你用高精度库有什么意义吗
    bli22ard
        24
    bli22ard  
       111 天前
    @UnluckyNinja #23 你的 0.3000004 ,不是计算得来,那是怎么得来的?是输入吗,为什么输入的不是 0.3 ,而输入 0.3000004 ?如果输入的是 0.3000004 ,显示成 0.3 是不是有问题?你的假设是不是有问题?
    635925926
        25
    635925926  
       111 天前
    @dssxzuxc #11 那也得正确啊,不能为了写起来简单,就忽略正确性
    lvlongxiang199
        26
    lvlongxiang199  
       111 天前
    太投机取巧了, 输入一个大点的 int64 就精度丢失了 https://imgur.com/a/x6FFD5i
    Ketteiron
        27
    Ketteiron  
       111 天前
    @635925926 #25 0.1.2 版本已经修复了这个问题了。
    除此之外还有相关的 3 个 pr ,都是用的 toFixed ,简单易懂可以去除一堆边界条件,但是作者都没有接受,依旧采用土法硬算舍入值。
    我对浮点计算没有深入研究,理论上 toFixed 处理 x.xxx9 和 x.xxx0 是完全正确的,而作者看起来似乎在硬扣性能,只有某些条件下才会回退到 toFixed 。
    作者的代码实现还有个多余的
    if (result === '0') {
    result = '0'
    }
    完全没看懂在干啥
    现在我是选了个其中一个 fork ,copy 一下放进 utils 里。
    UnluckyNinja
        28
    UnluckyNinja  
       111 天前
    @bli22ard #24 3000004/1000000 得来的可不可以?来自于网络的数据源很难理解吗?你管人家怎么来的,现在问题就是要展示这个数据,而你非要给计算上高精度库,关键是这个情景下就不涉及计算啊。
    楼主热得快炸了,你就非得让楼主开空调?
    UnluckyNinja
        29
    UnluckyNinja  
       111 天前
    @bli22ard #24 不过这么多人都看错,不怪你,得怪作者毫无必要地放了很多计算过程当作例子,网站文本存在误导,“修复浮点精度问题”但实际上只是显示上,而不是给出一个精确的计算结果。
    然后 11 楼举了的例子也有一定误导,我用高精度库了更可能是因为我需要准确的结果,而不是为了数字转字符串看起来好看,这种情况下根本没有可比性。
    这个库本身就是只有 number 类型到 string 类型转换,这么一个目的和功能。给定一个数字,返回一个字符串,就这么简单,甭管输入哪里来的,用户提供的,库作者想管也管不了。做 OJ 的时候也没人问 input 怎么来的吧。

    不过本来我也不太看好这个库,为了这点功能徒增太多了复杂实现,
    如果本身就要精度正确,那直接上精度库就好了,
    如果要裁剪小数部分,那 toFixed 就可以了,去末尾 0 那就再加个正则替换。
    如果为了智能判断高熵部分并展示……我不知道什么情况下会有这样一个需求,为什么要去在乎一个比 epsilon 小很多的噪音,就算如此,展示比 epsilon 小的值,为什么要做字符比较而不是基于数学方式去判断(例如 for 循环递增提取 5 位移到小数点后,tofixed(5)判断是否等于 1 或 0 ,而不是连续比较 5 个 0 或者 5 个 9 )
    我很怀疑原作者的精神状态……
    bli22ard
        30
    bli22ard  
       111 天前
    UnluckyNinja
        31
    UnluckyNinja  
       111 天前 via Android
    @bli22ard #30 是的这些全是误导,他又不处理计算,都是 js 运行时自己计算,运算结果作为真正参数调用这个库函数,放这些算式纯纯误导。
    他本身就是想解决“显示”有精度误差的数字的问题(作为其中一个功能但不是唯一功能),但正常人都会去想引入高精度库直接解决精度问题,而引入高精度库并不能解决“输入本身是一个有噪音部分小数的显示问题”,which 正是这个库“真正想解决的问题”(尽管他的实现让人感觉是不是被 AI 带坏了)
    但愿你绕过来了
    heiya
        32
    heiya  
       110 天前
    @mrlmh00 #7 银行家舍入法
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     945 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 18:53 PVG 02:53 LAX 10:53 JFK 13:53
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86