基本はもう身についていますね。\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)
最後のパターンは、「ドル記号、1桁以上の数字、その後にドットと2桁の数字からなる省略可能なグループ」という意味です。
注意: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はもう一歩進んで、各マッチに対してクオテーションを実行し、その結果を集めます。クオテーションはマッチのstartインデックス、endインデックス、そして文字列全体を受け取ります。subseqがその3つからマッチしたテキストを作り、それを変換します。
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は、各マッチに対してクオテーションを実行しますが、何も集めません。
小さな会社の帳簿を付けていると、毎日の取引は整っていないテキストの行として届きます。ここでは正規表現を使って金額を検証し、目印となる記号ごとに数値を抜き出し、よく確認する必要がある行に印を付けます。
正しい形式の金額は、$、1つ以上の数字、そして省略可能な小数部分でできています。小数部分は、ドットとそれに続くちょうど2桁の数字です。$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