JavaScirpt Currency Conversion to Thousandths Regular (Non-Capture Group Matching Explained)

Sorry, this article is not written in English and has not been translated into English yet .You can view this article in English with Google Translate, or please come back later.

如果给你一串数字,需要把他转换成货币的千分位格式,你会如何去做?比如:123123123 -> 123,123,123

1. 一个有意思的正则表达式的由来

这其实是个陈年老问题了,但是不知为何最近的出镜率特别高,所以决定这里讨论一下。

先看一种传统的思维:从右侧起每隔三位加一个逗号。于是就有了下面的方法:

function money(num){
    // 先把数字换成字符串,然后转换成数组,反转之后,再组合成字符串
    var reverseStr = num.toString().split('').reverse().join('');
    // 用正则替换,每隔3位加一个逗号
    reverseStr = reverseStr.replace(/(\d{3})/g,'$1,');
    // 处理正好三位的情况,如 123 -> ,123
    reverseStr = reverseStr.replace(/\,$/,'');
    // 把加了逗号的字符串反转回正常的顺序
    reverseStr = reverseStr.split('').reverse().join('');
    return reverseStr;
}

虽然这个方法能满足我们的需求,但是或多或少感觉有些low,也不是我们今天讨论的重点。

我们今天尝试使用一句简短的正则搞定这个问题,先上代码:

function money(num){
    return (''+num).replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
}

很简单的一个正则/(\d)(?=(\d{3})+(?!\d))/就搞定了一切。正则虽然短,但是并不简单,今天的目的,其实就是和大家一起来研究研究这个正则的内容。

我们先讨论这里涉及到的3个概念:

  1. 正则匹配的lastIndex
  2. 正则中形如(?=exp)零宽度正预测先行断言
  3. 正则中形如(!=exp)零宽度负预测先行断言

1.1 lastIndex

lastIndex:指的是上一次匹配的结果位置,也就是下一次匹配开始的位置,默认情况下为0(需要强调的是,只有在匹配模式是g或者y的情况下才有效,否则,每次匹配完成之后,lastIndex都会变成0)。这里之所以强调lastIndex是因为后面的断言(也叫非捕获)匹配会影响lastIndex的值,我们先看例子:

let re = /\d\d/g;
let str = '0123456789';
console.log(re.lastIndex);
// 0 
// 默认情况下 lastIndex 的值为0
console.log(re.exec(str));
// ["01",...] 匹配到了01
console.log(re.lastIndex);
// 2 
// 由于第一次匹配的结果是01,接下来的匹配应该从01之后的2开始,所以此时的lastIndex为2

同理,如果也可以手动修改lastIndex的值,匹配的结果也会受到影响。

let re = /\d\d/g;
let str = '0123456789';
re.lastIndex = 5;
// 手动修改 lastIndex 的值为5,下次匹配从第五未开始
console.log(re.exec(str));
// ["56",...]
console.log(re.lastIndex);
// 7 匹配完成之后,lastIndex自动修改到匹配的结果之后

1.2 (?=exp) 零宽度正预测先行断言 与 (?!exp) 零宽度负预测先行断言

第一次听到这个名字的感觉就好像不会中文一样,不知所云,内心一万只羊驼奔过。

在定义上它是指:它断言自身出现的位置的后面能匹配表达式exp,[一脸懵逼]表示我第一次没有看懂。

还是举一个简单的例子帮助大家理解,假设有人告诉你请你去找一个骑车的人,然后把这个人带过来(车不需要)你会怎么做?这句话抽象出来就其实就是用人骑车去匹配正则/人(?=骑车)/,结果要的是人而不需要车。

也正是由于上面括号内的表达式对结果没有影响,他们也属于非捕组获匹配

举个例子

var re = /ap(?=ple)/g;
console.log(re.exec('I like apple not app!'));
// ["ap", index: 7, input: "I like apple not app!"]
console.log(re.lastIndex)
// 9
console.log(re.exec('I like apple not app!'));
// null
console.log(re.lastIndex)
// 0

在上述例子中,第一个.exec会找到句子中ple之前的ap,那么第7-11个字符apple就符合我们的条件,但是由于(?=ple)是非捕获的,所以ple的并没有被计算到结果中,自然ple这3个字符也没有影响到lastIndex,所以lastIndex的值为 7+2=9 ,而不是7+5=11;

这里的非捕获很容忍让人误解,所以再强调一遍:

!!非捕获不会影响lastIndex的值!!

如果你明白了,请在脑海想一下下面的2个题目的结果是什么:

题目一

var re = /ap(?=ple)pie/g;
console.log(re.test('applepie'))

题目二

var re = /ap(?=ple)plepie/g;
console.log(re.test('applepie'))

不要作弊哦

答案是 ↓ ↓ ↓

题目一:false

题目二:true

如果没有猜对的话我们一起来看一下为什么:

实际上我们可以把/ap(?=ple)pie/分成2部分 ap(?=ple) + pie,在匹配字符串applepie的时候,经过了以下的步骤:

  1. 一开始lastIndex的值为0,ap(?=ple)匹配applepie 中的appleap字段,此时的结果是ap,lastIndex=2。
  2. 第一步过后lastIndex=2,后面的表达式是pie,表示从第二个字符开始后面应该紧接着的是pie,但是实际上第二个字符后面的是ple,这样条件就满足不了了,所以返回的就是false。  同理第二个正则/ap(?=ple)plepie/的第二步表示的是从第二个字符开始后面紧跟着的是plepie,完全符合我们的给出的字符串,所以结果是true

对于(?!exp) 零宽度负预测先行断言就不用多说了,他表示后面不跟着exp,同样也是非捕获(比如:.+?(?!xyz) 去匹配uvwuvwxyz的话只会匹配第一个)

2. /(\d)(?=(\d{3})+(?!\d))/的匹配步骤

说到这里如果你还是很迷茫的话,那么我们再说详细举一个例子来说明上面的正则是如何工作的。

我们以'1234567.88'.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,')为例。

首先解释一下(?=(\d{3})+(?!\d))的意思,他表示某个匹配规则之后,有一个或者多个数字组+,每组由3个数字\d{3}组成(比如 123456,123,123456789,这些都是由个数字组组成的字符串),并且数字组之后不是数字(?!\d)(这个是用来找到结尾,只要后面不是数字我们都认为是结尾)。

最后我们用图表来解释这个过程:

index (\d)也就是$1 的值 (?=(\d{3})+(?!\d))匹配的结果 lastLindex 字符串结果
0 '1' (234)(567) 1 '1,234567'
1 '2' -- (24567无法分成2租) 2 '1,234567'
2 '3' -- (4567无法分成2租) 3 '1,234567'
3 '4' (567) 4 '1,234,567'
... ... ... ... ...
9 '8' -- 10 '1,234,567'

好了,就说这么多了,如果还有疑问的话,可以再后面给我留言。

61080
  • logo
    • HI, THERE!I AM MOFEI

      (C) 2010-2024 Code & Design by Mofei

      Powered by Dufing (2010-2020) & Express

      IPC证:沪ICP备2022019571号-1