『问题探究』千分位分割和零宽断言

140 阅读4分钟

在学千分位分割的过程中,有一种方案是使用正则表达式。

function formatNumberWithCommas(number) {
  const fixedNum = number.toFixed(20);
  let digitCount = 20;
  for (let i = fixedNum.length - 1; i >= 0; i--) {
    if (fixedNum[i] === '0') {
      digitCount--;
    } else {
      break;
    }
  }
  const parts = number.toFixed(digitCount).split('.');
  parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return parts.join('.');
}

const number = 126536536536.276376273265;
const formattedNumber = formatNumberWithCommas(number);
console.log(formattedNumber); // "126,536,536,536.276376273265"

核心逻辑是中间的正则表达式,chatgpt 给我解释了:

  1. \B:表示非单词边界,匹配不在单词边界的位置。这确保我们在插入逗号时不会影响单词的边界。
  2. (?=(\d{3})+(?!\d)):这是一个正向断言,表示匹配在当前位置后面的内容,但不会消耗字符串中的字符。它的含义是在当前位置后面查找至少一组连续的三个数字,且这些数字后面不能跟着另一个数字。这保证了我们只会在数字之间插入逗号。
  3. (?!\d):这是一个负向断言,表示在当前位置后面查找不跟着数字的内容。这是为了确保我们在插入逗号时不会破坏原有的数字格式。

综合起来,这个正则表达式会在每三个连续的数字之间插入逗号,从而实现千分位分隔。

这里出现了两个词:正向断言负向断言。它们存在的意义是匹配位置,不会消耗字符。

零宽断言是个啥

零宽断言(Zero-Width Assertions)是正则表达式中一种高级的匹配技术,用于指定一个位置的前面或后面应该满足的条件,而不会实际匹配或消耗字符。换句话说,它们允许你在一个位置上进行条件匹配,但不会在字符串中移动位置。

零宽断言并不会真正匹配字符,而只是查看位置是否满足特定的条件。它们在处理复杂的匹配需求时非常有用,例如在特定位置插入内容、进行条件判断等。

在正则表达式中,有以下四种主要类型的零宽断言:

  1. 正向肯定断言(Positive Lookahead Assertion):(?=...) 正向肯定断言用于查看在当前位置后面是否满足给定条件。
  2. 正向否定断言(Negative Lookahead Assertion):(?!...) 正向否定断言用于查看在当前位置后面是否不满足给定条件。
  3. 反向肯定断言(Positive Lookbehind Assertion):(?<=...) 反向肯定断言用于查看在当前位置前面是否满足给定条件。
  4. 反向否定断言(Negative Lookbehind Assertion):(?<!...) 反向否定断言用于查看在当前位置前面是否不满足给定条件。

这些断言在匹配的过程中不会改变字符串的位置,它们只是在特定位置上进行条件判断。通过使用零宽断言,你可以在不匹配实际字符的情况下,更加精确地指定匹配的位置。

replace 为什么是插入

在正则表达式中,当我们使用 \B(?=...) 来查找位置时,我们实际上是在匹配一个零宽断言(zero-width assertion),而不是匹配实际的字符。这意味着,我们在找到位置后,并不会直接替换这个位置上的字符,而是在这个位置前面插入逗号。

在正则表达式的 replace 方法中,首先会匹配所有满足条件的位置,然后将这些位置替换为指定的内容。换句话说,replace 方法会首先找到所有要替换的位置,然后在一次性替换这些位置上的内容

仔细理解零宽断言

哎,看了上面chatgpt的解释,我对断言的运作还是云里雾里的,尤其是不理解为什么要有(!=\d),只能上regexr试一试了。

image.png 下方显示了插入的位置,好嘞,我们来分析一下为什么是这些位置。

1234567 890:只看890,满足三位数字出现一次(正向断言);890的后面啥也没有,满足不是数字(负向断言)。所以这个位置是满足的。

1234 567890:只看567890,满足三位数字出现两次;同理,后面啥也没有,满足不是数字。所以这个位置是满足的。

后面的就不分析了。

假如把(!=\d)去掉:

image.png

123456 7890:从7出发,找到有789三个数字,这个位置就满足了,因为此时并没有要求后面不能是数字。

12345 67890:从6出发,找到有678三个数字,这个位置就满足了。

其余同理,所以(!=\d)是必须的。“不是数字的字符”被限制到整个数的尾部,再结合?=,就可以找到距离末尾距离为3、6、9...的位置,也就是千分位分隔点。