你已经掌握了基础知识:像\d和[A-Z]这样的字符类,像{4}和+这样的量词,以及matches?、all-matching-subseqs和re-replace这几个词。本练习会补充正则表达式中描述结构和上下文的部分。它们都在同一个 regexp 词汇表里,用同样的R/ /字面量书写。
锚点匹配的是一个位置,而不是某个字符:
^ ! the start of the string
$ ! the end of the string
\b ! a word boundary
因为$表示字符串的末尾,所以要表示字面意义上的美元符号时,要写成\$。
单词边界(\b)是单词字符与非单词字符相接的位置。单词字符包括字母、数字和下划线;其他一切(空格、标点以及字符串的两端)都算作边界。所以\bcat\b只在cat单独成词时才匹配它:
USING: regexp ;
"cat nap" R/ \bcat\b/ re-contains? . ! => t
"scatter" R/ \bcat\b/ re-contains? . ! => f (cat sits inside a word)
圆括号( )把模式的一部分分组,让量词,或者|(“这个或那个”)作用于整个分组:
USING: regexp ;
"abab" R/ (ab)+/ matches? . ! => t (+ applies to "ab")
"dog" R/ cat|dog/ matches? . ! => t (either word)
"$100.50" R/ \$\d+(\.\d{2})?/ matches? . ! => t
"$100" R/ \$\d+(\.\d{2})?/ matches? . ! => t (the group is optional)
最后一个模式的意思是:“一个美元符号,一个或多个数字,然后是一个可选的分组,里面是一个点加两位数字。”
注意:在 Factor 中,分组只是组织模式的结构。与某些正则工具不同,你无法再取出分组所匹配的文本,这里没有捕获组提取,也不支持像
\1这样的反向引用。要从某个标志周围取出文本,请改用环视(见下文)。
环视检查当前位置紧邻的前面或后面是什么,而不把这些字符算作匹配的一部分。它是“零宽”的:它检测的内容不包含在结果里。
(?=...) ! lookahead: ... must follow
(?!...) ! negative lookahead: ... must NOT follow
(?<=...) ! lookbehind: ... must come before
(?<!...) ! negative lookbehind: ... must NOT come before
环视是有选择性的:同样的字符是否匹配,取决于它旁边是什么。单纯的\d+会抓取每一个数字;后顾和前瞻只保留邻居正确的那些:
USING: regexp ;
"3 for $10 or 10 for $30" R/ \d+/ all-matching-subseqs .
! => { "3" "10" "10" "30" } (every number)
"3 for $10 or 10 for $30" R/ (?<=\$)\d+/ all-matching-subseqs .
! => { "10" "30" } (only the prices — the bare 3 and 10 drop)
"buy 2 at 15% off, 4 at 30% off" R/ \d+(?=%)/ all-matching-subseqs .
! => { "15" "30" } (only the discounts — the bare 2 and 4 drop)
在结尾的/后面加一个字母,可以改变整个模式的行为。i让它不区分大小写:
USING: regexp ;
"REFUND" R/ refund/i matches? . ! => t
"Refund" R/ refund/i matches? . ! => t
all-matching-subseqs把匹配到的文本交给你。map-matches更进一步:它会对每一个匹配运行一个 quotation,并把结果收集起来。这个 quotation 会收到匹配的start下标、end下标和整个字符串;subseq把这三者变成匹配到的文本,然后由你来转换:
map-matches ( string regexp quot: ( start end string -- obj ) -- seq )
USING: math regexp sequences ;
"a1 b22 c333" R/ \d+/ [ subseq length ] map-matches .
! => { 1 2 3 } (the length of each run of digits)
它只产生副作用的近亲 each-match,会对每个匹配运行一个 quotation,但不收集任何结果。
你为一家小公司记账,每天的交易记录都是一行行杂乱无章的文本。你将使用正则表达式来验证金额、按标记符号提取数字,并标记出需要进一步查看的行。
格式正确的金额由一个$、一个或多个数字,以及可选的小数部分组成:小数部分是一个小数点加上恰好两位数字,例如$100或$100.50,但不是$100.5。定义valid-amount?,当整个字符串都是格式正确的金额时返回t。
"$100.50" valid-amount? . ! => t
"$100" valid-amount? . ! => t
"$100.5" valid-amount? . ! => f
定义dollar-amounts,返回该行中紧跟在$后面的每一个数字(只包含数字,不含$),并保持原有顺序。
"spent $100 and $25 today" dollar-amounts .
! => { "100" "25" }
定义percentages,返回每一个紧跟%的数字。
"up 5% then down 12%" percentages .
! => { "5" "12" }
定义flagged?,当一行中出现任意大小写组合的refund或chargeback时返回t。
"Issued a REFUND today" flagged? . ! => t
"a normal sale" flagged? . ! => f