你已經熟悉了基本概念:像是 \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