如何使用JavaScript将数字四舍五入到任意位数的有效数字?

13

我尝试了下面的示例代码

function sigFigs(n, sig) {
    if ( n === 0 )
        return 0
    var mult = Math.pow(10,
        sig - Math.floor(Math.log(n < 0 ? -n: n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
 }

但是对于输入sigFigs(24730790,3)返回的结果为24699999.999999996,对于输入sigFigs(4.7152e-26,3)返回的结果为4.7200000000000004e-26,该函数并不能正常工作。

如果有人有可用的示例,请分享。谢谢。

6个回答

29

你可以尝试JavaScript内置方法 -

Number( my_number.toPrecision(3) )

对于您的情况,请尝试:

Number( 24730790.0.toPrecision(5) )

供您参考和实际操作,您可以查看链接


1
可以工作,但不适用于整数(在Chrome中)。使用 parseFloat(number.toPrecision(precision))。感谢@Gianluca Casati。 - B Seven

7

首先感谢大家,如果没有这些分享的代码片段,这将是一项艰巨的任务。

我的增值功能是以下代码片段(请参见下面的完整实现)

parseFloat(number.toPrecision(precision))

请注意,如果数字是10000,精度是2,那么number.toPrecision(precision)将会是'1.0e+4',但parseFloat可以理解指数符号表示法。
值得一提的是,使用上述使用Math.pow和对数的算法,在测试用例formatNumber(5, 123456789)上在Mac(node v12)上成功运行,但在Windows(node v10)上出现错误。这很奇怪,所以我们得到了上面的解决方案。
最终,我找到了这个决定性的实现方法,并利用本帖中提供的所有反馈意见。假设我们有一个名为formatNumber.js的文件,其中包含以下内容。
/**
 * Format number to significant digits.
 *
 * @param {Number} precision
 * @param {Number} number
 *
 * @return {String} formattedValue
 */

export default function formatNumber (precision, number) {
  if (typeof number === 'undefined' || number === null) return ''

  if (number === 0) return '0'

  const roundedValue = round(precision, number)
  const floorValue = Math.floor(roundedValue)

  const isInteger = Math.abs(floorValue - roundedValue) < Number.EPSILON

  const numberOfFloorDigits = String(floorValue).length
  const numberOfDigits = String(roundedValue).length

  if (numberOfFloorDigits > precision) {
    return String(floorValue)
  } else {
    const padding = isInteger ? precision - numberOfFloorDigits : precision - numberOfDigits + 1

    if (padding > 0) {
      if (isInteger) {
        return `${String(floorValue)}.${'0'.repeat(padding)}`
      } else {
        return `${String(roundedValue)}${'0'.repeat(padding)}`
      }
    } else {
      return String(roundedValue)
    }
  }
}

function round (precision, number) {
  return parseFloat(number.toPrecision(precision))
}

如果你使用tape进行测试,这里有一些基本的测试。
import test from 'tape'

import formatNumber from '..path/to/formatNumber.js'

test('formatNumber', (t) => {
  t.equal(formatNumber(4, undefined), '', 'undefined number returns an empty string')
  t.equal(formatNumber(4, null), '', 'null number return an empty string')

  t.equal(formatNumber(4, 0), '0')
  t.equal(formatNumber(4, 1.23456789), '1.235')
  t.equal(formatNumber(4, 1.23), '1.230')
  t.equal(formatNumber(4, 123456789), '123500000')
  t.equal(formatNumber(4, 1234567.890123), '1235000')
  t.equal(formatNumber(4, 123.4567890123), '123.5')
  t.equal(formatNumber(4, 12), '12.00')
  t.equal(formatNumber(4, 1.2), '1.200')
  t.equal(formatNumber(4, 1.234567890123), '1.235')
  t.equal(formatNumber(4, 0.001234567890), '0.001235')

  t.equal(formatNumber(5, 123456789), '123460000')

  t.end()
})

5
如何自动进行类型转换,以处理指数表示法?
f = (x, n) => +x.toPrecision(n)

测试:

> f (0.123456789, 6)
0.123457
> f (123456789, 6)
123457000
> f (-123456789, 6)
-123457000
> f (-0.123456789, 6)
-0.123457
> f (-0.123456789, 2)
-0.12
> f (123456789, 2)
120000000

它返回一个数字而不是字符串。


谢谢,非常好。这让我省了不少功夫。 - Leo

2

很不幸,当数字> 10时,内置方法会给出荒谬的结果,如指数表示法等。

我写了一个函数,可以解决这个问题(可能不是最优雅的编写方式,但是它可以工作):

Original Answer翻译成"最初的回答"

function(value, precision) {
  if (value < 10) {
    value = parseFloat(value).toPrecision(precision)
  } else {
    value = parseInt(value)
    let significantValue = value
    for (let i = value.toString().length; i > precision; i--) {
      significantValue = Math.round(significantValue / 10)
    }
    for (let i = 0; significantValue.toString().length < value.toString().length; i++ ) {
      significantValue = significantValue * 10
    }
    value = significantValue
  }
  return value
}

如果你更喜欢使用科学计数法来表示较高的数字,可以使用toPrecision()方法。

最初的回答:


1
我尝试在(123456, 3)上使用您的函数,结果返回的是124000而不是123000。可能是单位舍入一直持续到百位数。 - coyotte508

2

使用纯数学(即不涉及字符串)

您可以通过先计算数字包含的最大十的幂,然后使用该值计算应将数字舍入到哪个精度来将其舍入为任意数量的有效数字。

/**
 * Rounds a value to a given number of significant figures
 * @param value The number to round
 * @param significanFigures The number of significant figures
 * @returns The value rounded to the given number of significant figures
 */
const round = (value, significantFigures) => {
  const exponent = Math.floor(Math.log10(value))
  const nIntegers = exponent + 1
  const precision = 10 ** (nIntegers - significantFigures)
  return Math.round(value / precision) * precision
}

在上述代码中,precision表示我们想要舍入到的最近倍数。例如,如果我们想要舍入到最接近的100的倍数,则精度为100。如果我们想要舍入到最接近0.1的倍数,即保留一位小数,那么精度为0.1。 exponent是值中包含的10的最大幂的指数。如果您对该指数的来源感兴趣,请继续阅读以下内容。 nIntegers是值中整数(小数点左侧的数字)的数量。
一些例子:
> round(173.25, 1)
200
> round(173.25, 2)
170
> round(173.25, 3)
173
> round(173.25, 4)
173.3
> round(173.25, 5)
173.25

一个直观而富有教育性的解释

通常,我们可以通过首先将数字向一个方向“移动小数点”(除法),然后将数字四舍五入到最近的整数,最后将小数点移回其原始位置(乘法),以达到给定精度的四舍五入。

const rounded = Math.round(value / precision) * precision

例如,要将一个值舍入到最接近的10的倍数,精度设置为10,然后我们得到:
> Math.round(173.25 / 10) * 10
170

同样地,如果我们想将一个值舍入到小数点后一位,即找到最接近的 0.1 的倍数,精度应设置为 0.1

> Math.round(173.25 / 0.1) * 0.1
173.3

在这里,"精度" 简单地指的是 "我们想要四舍五入的最接近的倍数"。
那么我们如何利用这个知识来将一个值四舍五入到给定数量的有效数字呢?
我们需要解决的问题是确定我们应该四舍五入到的精度,根据有效数字的数量来确定。假设我们想要将值 12345.67 四舍五入到三个有效数字。在这种情况下,我们如何确定精度应该是 100?
精度应该是 100,因为 Math.round(12345.67 / 100) * 100 得到 12300,即四舍五入到三个有效数字。
实际上,解决这个问题非常简单。
基本上,我们需要做的是:1)确定小数点左边有多少位数字,然后2)根据这个数字确定在四舍五入数字之前 "移动小数点" 的步骤数。

我们首先计算数字整数部分的位数(即5位数字),然后减去我们要舍入到的有效数字的位数(3位数字)。结果为5-3=2,这是我们应该将小数点向左移动的步数(如果结果为负数,则应将小数点向右移动)。

为了将小数点向左移动两个步骤,我们必须使用精度10^2 = 100。换句话说,结果给出了我们应该将10提高到的幂,以便获得精度。

n_integer = "number of digits in the integer part of the number" = 5
n_significant = "the number of significant figures we want to round" = 3
precision = 10 ** (n_integer - n_significant)

就是这样!

但是,等一下!你还没有向我们展示如何编写代码!而且,你说我们不想使用字符串。那么,我们如何计算数字部分的位数,而不将其转换为字符串?好吧,我们不必使用字符串。数学来拯救我们!

我们知道一个实数 v 可以表示为十的幂(使用十进制系统)。也就是说,v = 10^a。如果我们现在只取 a 的整数部分,称之为 a',新值 v' = 10^a' 将是包含在 v 中的最大的 10 的幂。v 中的数字位数与 v' 中的数字位数相同,即 a' + 1。因此,我们已经证明了n_integer = a' + 1,其中a' = floor(log10(v))

在代码中,它看起来像:

const exponent = Math.floor(Math.log10(v)) // a'
const nIntegers = exponent + 1

正如我们之前所说,精度是

const precision = 10 ** (nInteger - nSignificant)

最后,四舍五入

return Math.round(value / precision) * precision
例子
假设我们想将值v = 12345.67四舍五入到1个有效数字。为了使上述代码正常工作,精度必须是precision = 10000 = 10^(n_integers - 1)
如果我们想要四舍五入到6个有效数字,精度必须是precision = 0.1 = 10^(n_integers - 6)
通常,精度必须是precision = 10^(n_integers - n_significant)

一个有趣的副作用

使用这个代码和知识,你可以将一个数字四舍五入到任何值的最接近倍数,而不仅仅是普通的、无聊的10的幂次方(即{..., 1000, 100, 10, 1, 0.1, 0.01, ...})。不,使用这个方法,你可以将一个数字四舍五入到最接近的0.3的倍数。
> Math.round(4 / 0.3) * 0.3
3.9

0.3 * 13 = 3.9,这是最接近 40.3 的倍数。


有时候,当生成的数字被转换回字符串时,所提供的代码将会给出一些令人不满意的结果;例如round(100, 8)将输出100.00000000000001。我观察到,可以通过首先进行乘法而不是除法来获得更好的结果,即将10 ** (…)更改为10 ** -(…),然后再执行Math.round(value*precision)/precision。(我承认我不完全明白为什么会这样 - 这些微妙的浮点数学诡计往往超出了我的理解能力。) - Etienne Dechamps
提议的代码还存在一个小问题,即在负数上会返回NaN。这可以通过将Math.log10(value)更改为Math.log10(Math.abs(value))来简单解决。 - Etienne Dechamps
round(1, 5) = 0.9999999999999999
真是喜欢 JavaScript。
- TrophyGeek

1

如果您想在小数点左侧指定有效数字,并将多余的占位符替换为 T B M K,则可以这样做。

// example to 3 sigDigs (significant digits)
//54321 = 54.3M
//12300000 = 12.3M

const moneyFormat = (num, sigDigs) => {
   var s = num.toString();
   let nn = "";
   for (let i = 0; i <= s.length; i++) {
      if (s[i] !== undefined) {
         if (i < sigDigs) nn += s[i];
         else nn += "0";
      }
   }
   nn = nn
      .toString()
      .replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
      .replace(",000,000,000", "B")
      .replace(",000,000", "M")
      .replace(",000", "k");
   if (
      nn[nn.length - 4] === "," &&
      nn[nn.length - 2] === "0" &&
      nn[nn.length - 1] === "0"
   ) {
      let numLetter = "K";
      if (parseInt(num) > 999999999999) numLetter = "T";
      else if (parseInt(num) > 999999999) numLetter = "B";
      else if (parseInt(num) > 999999) numLetter = "M";
      console.log("numLetter: " + numLetter);
      nn = nn.toString();
      let nn2 = ""; // new number 2
      for (let i = 0; i < nn.length - 4; i++) {
         nn2 += nn[i];
      }
      nn2 += "." + nn[nn.length - 3] + numLetter;
      nn = nn2;
   }

   return nn;
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接