如果你写的方法f能经过下面参数的考验,请跳过此篇文章。
f(null); // invalid value
f(undefined); // invalid value
f(); // invalid value
f([12.888]); // invalid value
f('abc') // invalid value
f(NaN) // invalid value
f(0.1+0.2); // 0.30
f(0.3 - 0.2); // 0.10
f(1e3); // 1,000.00
f('1e3') // 1,000.00
f(123456) // 123,456.00
f(123456.456) // 123,456.46
f(12.899) // 12.90
f(12.999) // 13.00
f(10.055) // 10.06
f(10.005) // 10.01
f(10.105) // 10.11
f(10.155); // 10.16
f(10.105) // 10.11
f(0.0001) // 0.00
f(-10.105) // -10.11
f(.5) // 0.50
f(0/0) // invalid value
f(12/0) // invalid value
f(10 ** -4) // 0.00
如果你的方法过不去,请参阅下文。
一:方法详述
思路:
- 参数只支持数字(支持负数)的字符串和数字形式,别的暂不支持。
- 参数四舍五入处理,最多保留两位小数
- 参数整数位千分位添加分隔符(,),小数位不足两位的尾部用0补足。
1、参数只支持数字
有些同学喜欢上来先 参数.toString(),先别急撒。
万一参数是null/undefined,有时候是后台返给你的。当然,报错也在悄悄等着你。
你又默默的传了一个[12.888],数组自定义了toString方法,结果返回'12.888'。这时候程序反而能正常执行,hh....。
so,我们来约定,参数只支持数字,其余的直接返回'invalid value'。这个数字限定在数值和字符串的类型下,同时,isFinite(参数)返回true即可(这样就可以整治各种花里花哨)。
isFinite会隐性的先将参数转成数值。它会过滤掉NaN、Infinity、-Infinity。
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(2) // true
isFinite(-2) // true
isFinite(.5) // true
isFinite(1e3) // true
isFinite([1]) // true
isFinite([1,2]) // false
isFinite({}) // false
isFinite(new Date()) // true
isFinite(/x/) // false
Number.isFinite()也可,需手动将传参转成数值。
起初,计划参数转数值不是NaN就好,结果验证的时候,有个奇怪的 In,fin,ity.00。也是给自己排雷了。so,果断用isFinite。
所以这第一步如下:
function numberWithCommas(x, num = 2) {
// 以下两个方法都可以过滤NaN 和 Infinity/-Infinity,任选其一
let bool = isFinite(x); // 方法一
// let bool = Number.isFinite(Number(x)); // 方法二
if ((typeof x === 'string' || typeof x === 'number')
&& bool) {
return addCommas(round(x, num), num)
} else {
return "invalid value"
}
}
2、参数四舍五入处理,最多保留两位小数
四舍五入保留两位,有同学举手说我会!toFixed方法。当然你说完这个答案后,你就不会了。
来来来,举栗说明。
// 结果挺正常
(10).toFixed(2) // "10.00"
10.005.toFixed(2) // "10.01"
// 结果就挺不正常
(10.055).toFixed(2) // "10.05"
(10.135).toFixed(2) // "10.13"
so,放弃toFixed()吧。
- 方式一:手写round方法,
- 方式二:站在巨人的肩膀上(这里计划用number-precision)
手写round方式,思路是先与100相乘(自定义times方法),四舍五入(Math.round())后,再除以100。
- 参数与100相乘,自定义了times方法。没直接用数学计算(参数 * 100)的原因是,在js中,浮点数的计算太不准确。
10.155*100 // 1015.4999999999999
10**-4 // 0.00009999999999999999
Math.pow(10,-4) // 0.00009999999999999999
0.1+0.2 // 0.30000000000000004
0.3-0.2 // 0.09999999999999998
- Math.round对正数很友好,如果你的传参是个负数,可能又有点问题。
console.log(Math.round(1.1)) // 1
console.log(Math.round(1.5)) // 2
console.log(Math.round(1.6)) // 2
console.log(Math.round(-1.1)) // -1
console.log(Math.round(-1.5)) // -1
console.log(Math.round(-1.6)) // -2
so,先把参数取绝对值,计算完,再把符号(如果是负数的话)还原回去。
所以这第二步,是从下面的 round 方法,任选其一。
第一种方式(NP)— 首选
// 安装 number-precision
npm install number-precision --save
// 使用
import NP from 'number-precision';
// 或
const NP = require('number-precision')
function round(num, decimal = 2) {
return NP.round(num, decimal)
}
第二种方式(自定义round方法,NP的低配版)
function round(num, decimal = 2) {
let base = Math.pow(10, decimal);
let result = Math.round(Math.abs(times(num, base))) / base;
if (num < 0 && result !== 0) {
result *= -1;
}
return result;
}
function times (num1, num2) {
const {val: val1, len: len1} = farmatVal(num1)
const {val: val2, len: len2} = farmatVal(num2)
return val1 * val2 / Math.pow(10, len1 + len2);
}
function farmatVal (num) {
// floatNoDot:浮点数去掉小数点
const floatNoDot = num => Number((num + '').replace('.', ''));
// decLength:浮点数小数的位数
const decLength = num => (((num + '').split('.'))[1] || '').length;
return {
val: floatNoDot(num),
len: decLength(num)
}
}
3、格式化返回。整数添加千分位分隔符,小数位不足两位的尾部用0补足
最后的主菜来了。千分位分隔,正则最方便也最简洁的方式。
但昧心自问,我会正则吗?我会吗?我会吗?这...不怎么会,简单的能看懂,有点难度的不在涉猎范围之内。
这第三步可从下面的addCommas方法,任选其一。当然你如果更会写格式化方法,来,笔给你。
第一种方式(正则)
function addCommas(x, num = 2) {
let [integer, decimals = ''] = (x + '').split('.')
// 添加千分位分隔符
integer = integer + ''
let pattern = /(-?\d+)(\d{3})/;
while (pattern.test(integer)) {
integer = integer.replace(pattern, '$1,$2')
}
// 尾部补零
decimals = (decimals + '0'.repeat(num)).slice(0, num)
return integer + '.' + decimals;
}
第二种方式(正则)
function addCommas(x, num = 2) {
// 尾部补零
x = x + ''
x = x.replace(/^(-?\d+)$/, '$1.')
let reg = new RegExp(`(\\.\\d{${num}})\\d*$`)
x = (x + '0'.repeat(num)).replace(reg, '$1')
// 添加分隔符
x = x.replace('.', ',')
let pattern = /(\d)(\d{3},)/;
while (pattern.test(x)) {
x = x.replace(pattern, '$1,$2')
}
let regDec = new RegExp(`,(\\d{${num}})$`)
x = x.replace(regDec, '.$1')
return x;
}
第三种方式(非正则):整数位数组每千位添加一个分隔符,再转为字符串
function addCommas(x, num = 2) {
// 符号位
let sign = x < 0 ? '-' : '';
let [
integer = '',
decimals = ''
] = (Math.abs(x).toString()).split('.');
// 整数位
let integerArr = [...integer];
for (let len = integerArr.length, i = len - 1; i > 0; i--) {
if ((len - 1 - i) % 3 === 2) {
integerArr.splice(i, 0, ',')
}
}
// 小数位补零
decimals = decimals.slice(0, num).padEnd(num, '0')
return sign + integerArr.join('') + '.' + decimals;
}
第四种方式(非正则):利用数组的reduceRight方法,累积的过程。
function addCommas(x, num = 2) {
let sign = x < 0 ? '-' : '';
let [
integer = '',
decimals = ''
] = (Math.abs(x).toString()).split('.');
// 整数位
let integerStr = ([...integer]).reduceRight((result, int, index, arr) => {
return int + ((arr.length - 1 - index) % 3 === 0 ? ',': '') + result
})
// 小数位补零
decimals = (decimals + '0'.repeat(num)).slice(0, num);
return sign + integerStr + '.' + decimals;
}
二:方法验证
console.log(numberWithCommas(null)); // invalid value
console.log(numberWithCommas(undefined)); // invalid value
console.log(numberWithCommas()); // invalid value
console.log(numberWithCommas([12.888])); // invalid value
console.log(numberWithCommas('abc')); // invalid value
console.log(numberWithCommas(NaN)); // invalid value
console.log(numberWithCommas(0.1 + 0.2)); // 0.30
console.log(numberWithCommas(0.3 - 0.2)); // 0.10
console.log(numberWithCommas(1e3)); // 1,000.00
console.log(numberWithCommas('1e3')); // 1,000.00
console.log(numberWithCommas(123456)); // 123,456.00
console.log(numberWithCommas(123456.456)); // 123,456.46
console.log(numberWithCommas(12.899)); // 12.90
console.log(numberWithCommas(12.999)); // 13.00
console.log(numberWithCommas(10.055)); // 10.06
console.log(numberWithCommas(10.005)); // 10.01
console.log(numberWithCommas(10.105)); // 10.11
console.log(numberWithCommas(10.155)); // 10.16
console.log(numberWithCommas(-10.105)); // -10.11
console.log(numberWithCommas(.5)); // 0.50
console.log(numberWithCommas(0/0)); // invalid value
console.log(numberWithCommas(12/0)); // invalid value
console.log(numberWithCommas(10 ** -4)) // 0.00
基本实现了要求。唯一的问题是 JS语言中精度最多只能到53个二进制位,绝对值小于2的53次方的整数,即到,都可以精确表示,否则无法保持精度。由于2的53次方是一个16位的十进制数值,所以JavaScript 对15位的十进制数都可以精确处理。如果整数位 加 两位小数大于15位,结果可能不准确。
其次,金额格式化成2位小数,是常规操作。但对于某些值:罚息等,可能有三位小数的格式化需求。本文提供的格式化方法满足3位小数的处理。
The end.