【封装小技巧】数字处理函数的封装

未觉雨声
English
伸手请直接跳到【方法合集】~

这次我们来聊聊关于数字处理的一些问题。项目中对数字的处理一定是避不开的,毕竟数据就是由数字组成的嘛(大雾),所以对于一下常见的数字处理场景我们进行适当的封装也能有效简洁代码,下面就由简单到复杂的顺序来介绍几个。

将任意值转为有效数字

在一些场合,我们可能会得到一些类型不安全的值,并需要将其作为数字进行处理,这种场景在写库的时候尤为常见。当我们直接使用 parseFloat 转化时,在不合法数字时会得到 NaN,这将导致后续所有的运算全部变成 NaN,因此我们需要将其回退到 0 确保后续运算正常。

function toNumber(value: unknown) {
  const number = parseFloat(value as string)

  return Number.isNaN(number) ? 0 : number
}

将数值限定在一个范围

这个场景和上面很相似,同样是为了保证数字的正确性,需要将数字限定在一个特定的范围中,针对的是数字的值。

function boundRange(number: number | string, min: number, max: number) {
  return Math.max(min, Math.min(max, toNumber(number)))
}

这两个场景都是很简单的处理,旨在简化多次出现的繁杂代码。

将个位数变成两位

这个太简单,不多说了,像是在处理日期、时间上常常会出现这样的需求。

function doubleDigits(number: number) {
  return number < 10 ? `0${number}` : number.toString()
}

将数字格式化成三位阶

换而言之,就是常见的将数字按照三位一组分隔开的记数法,在提高一些大数字的可读性上,或者金额的显示上用的比较多:

function segmentNumber(number: number | string, separator = ',', segment = 3) {
  if (typeof number !== 'number') {
    number = parseFloat(number)
  }

  if (Number.isNaN(number)) return '0'

  let [integer, decimal] = String(number).split('.')

  const formatRegExp = new RegExp(`(\\d+)(\\d{${segment}})`)

  while (formatRegExp.test(integer)) {
    integer = integer.replace(formatRegExp, `$1${separator}$2`)
  }

  decimal = decimal ? `.${decimal}` : ''

  return `${integer}${decimal}`
}

将数字保留特定位数的小数

这是我们这一次的重磅选手,在 js 中将数字保留特定位数是个技术活,因为 js 的小数存在精度丢失问题,我们先来看一个例子:

当我们想把 17.275 这个小数遵循四舍五入保留至两位的时候,学过小学数学的都应该知道结果为 17.28,我们先来用 js 中最常规的做法 toFixed

17.275.toFixed(2)

image.png

js 引擎小学数学没学好 这就是小数精度丢失,具体原理这里就不展开了,还不了解的话应该快快地找找相关资料了。

我们通过下面代码,其实可以发现 toFixed 的表现和 Math.round 是类似的(猜测底层实现是一样的,未验证):

Math.round(17.275 * 10 ** 2)

image.png

同样的 .5 被直接舍弃而不是进一。

随后又出现了一些奇怪的处理方法,比如讲这个数在目标位数的基础上先扩大 10 倍再缩小 10 倍,再进行四舍五入从而规避精度丢失:

parseFloat(`${Math.round(17.275 * 10 ** 3 / 10) / 10 ** 2}`)

image.png

看上去好像挺正常的,但很可惜这只是针对 17.275 这个一个数字而已。

我们将原始数字改为 1.3335 并将目标位数改为 3,问题重新发生:

image.png

其实只要我们还用 Math.round 或者 toFixed 的方式来直接来处理四舍五入的问题,精度丢失的问题就始终无法规避。

为什么说 直接 呢,因为其实我们可以通过一些方式处理一下数字,再给到这些方法处理,就可以达到间接处理的效果,从而规避精度丢失。

其实这个方法很粗暴,我们都知道精度丢失无非就是大了一个或者小了一个非常小的小数,所以在 0.5 这个界线上会因为这个非常小的小数而导致舍或入的判断失准,那其实我们只需要在保留的目标位数的下一位上,一旦发现这个数是 5 就直接让他变成 6,其他情况就把这一位后面的部分裁掉,那这个很小的小数就不会影响到舍或入的判断了,来看代码:

function toFixed(number: number, decimal: number) {
  if (decimal === 0) return Math.round(number)

  let snum = String(number)
  const pointPos = snum.indexOf('.')

  if (pointPos === -1) return number

  const nums = snum.replace('.', '').split('')
  const targetPos = pointPos + decimal
  const datum = nums[targetPos]

  if (!datum) return number

  if (snum.charAt(targetPos + 1) === '5') {
    snum = snum.substring(0, targetPos + 1) + '6'
  } else {
    snum = snum.substring(0, targetPos + 2)
  }

  return parseFloat(Number(snum).toFixed(decimal))
}

image.png

方法合集

/**
 * 将任意值转成数字,NaN 的情况将会处理成 0
 * @param value - 需要转化的值
 */
export function toNumber(value: unknown) {
  const number = parseFloat(value as string)

  return Number.isNaN(number) ? 0 : number
}

/**
 * 讲小于 10 整数 N 变成 `0N` 的字符串,方法不会对入参校验
 * @param number - 需要处理的整数
 */
export function doubleDigits(number: number) {
  return number < 10 ? `0${number}` : number.toString()
}

/**
 * 将数字格式化为三位阶
 * @param number - 需要格式化的数字
 * @param segment - 分隔的位数,默认为 3
 * @param separator - 分隔的符号,默认为 ','
 */
export function segmentNumber(number: number | string, segment = 3, separator = ','): string {
  if (typeof number !== 'number') {
    number = parseFloat(number)
  }

  if (Number.isNaN(number)) return '0'

  let [integer, decimal] = String(number).split('.')

  const formatRegExp = new RegExp(`(\\d+)(\\d{${segment}})`)

  while (formatRegExp.test(integer)) {
    integer = integer.replace(formatRegExp, `$1${separator}$2`)
  }

  decimal = decimal ? `.${decimal}` : ''

  return `${integer}${decimal}`
}

/**
 * 讲一个实数保留一定的小数
 * @param number - 需要处理的实数
 * @param decimal - 需要保留的小数
 */
export function toFixed(number: number, decimal: number) {
  if (decimal === 0) return Math.round(number)

  let snum = String(number)
  const pointPos = snum.indexOf('.')

  if (pointPos === -1) return number

  const nums = snum.replace('.', '').split('')
  const targetPos = pointPos + decimal
  const datum = nums[targetPos]

  if (!datum) return number

  if (snum.charAt(targetPos + 1) === '5') {
    snum = snum.substring(0, targetPos + 1) + '6'
  } else {
    snum = snum.substring(0, targetPos + 2)
  }

  return parseFloat(Number(snum).toFixed(decimal))
}

/**
 * 将一个实数扩大一定的倍数并保留一定的小数
 * @param number - 要处理的实数
 * @param multiple - 要扩大的倍数
 * @param decimal - 要保留的小数
 */
export function multipleFixed(number: number, multiple: number, decimal: number) {
  return toFixed(number * multiple, decimal)
}

/**
 * 将一个数字限定在指定的范围内
 * @param number - 需要限定范围的数
 * @param min - 边界最小值,包含该值
 * @param max - 边界最大值,包含该值
 *
 * @returns 限定了范围后的数
 */
export function boundRange(number: number | string, min: number, max: number) {
  return Math.max(min, Math.min(max, parseFloat(number as string)))
}

往期传送门:

【封装小技巧】列表处理函数的封装
【封装小技巧】is 系列方法的封装

最后来推荐一下我的个人开源项目 Vexip UI - GitHub

一个比较齐全的 Vue3 组件库,支持全面的 css 变量,内置暗黑主题,全量 TypeScript 和组合式 Api,其特点是所有组件几乎每个属性都支持通过配置(传一个对象)来修改其默认值,这应该是目前其他组件库不具备的特性~

现正招募小伙伴来使用或者参与维护与发展这个项目,我一个人的力量非常有限,文档、单元测试、服务端渲染支持、周边插件、使用案例等等,只要你有兴趣都可以从各个切入点参与进来,非常欢迎~

这几期【封装小技巧】的内容源码都包含在了 @vexip-ui/utils 包下面,GitHub,这个包也有单独发布,不过目前还没有 Api 文档,可能需要直接查阅源码食用~

阅读 241

Vue3 组件库 VexipUI 作者,擅长 js 和 vue 系列技术,主攻前端(交互),稍微会一点点 Java(Spring Bo...

1.3k 声望
46 粉丝
0 条评论

Vue3 组件库 VexipUI 作者,擅长 js 和 vue 系列技术,主攻前端(交互),稍微会一点点 Java(Spring Bo...

1.3k 声望
46 粉丝
文章目录
宣传栏