ใน features 多いที่ Ruby 2.0 ส่งมอบ 2013 อันที่ผมสนใจน้อยสุดคือ regex engine ใหม่ Onigmo regex คือ regex ทำไมสนใจ Ruby implement ยังไง
แต่ Onigmo regex engine มี tricks น่าสนใจ รวมถึง conditional expressions ภายใน regex
โพสต์นี้ดำดิ่ง regex conditionals เรียนรู้ quirks ของ Ruby implementation และ tricks workaround limitations เริ่มเลย!
Groups & Capturing
เข้าใจ conditionals ใน regex ต้องเข้าใจ grouping และ capturing ก่อน
จินตนาการ list US cities:
Fayetteville, AR
Seattle, WA
อยากแยก city name จาก state abbreviation วิธีหนึ่ง multiple matches:
PLACE = "Fayetteville, AR"
# City: Match char ไม่ใช่ comma
PLACE.match(/[^,]+/) # => #
# Separator: Match comma และ optional spaces
PLACE.match(/, */) # => #
# State: Match 2-letter code ท้าย string
PLACE.match(/[A-Z]{2}$/) # => #
Work แต่ verbose ด้วย groups capture ทั้ง city และ state ด้วย regex เดียว
รวม regex ข้างบน ล้อมแต่ละส่วนด้วย parentheses Parens คือ grouping ใน regex:
PLACE = "Fayetteville, AR"
m = PLACE.match(/([^,]+)(, *)([A-Z]{2})/)
# => #
เห็น capture ทั้ง city และ state เข้าถึงด้วยการ treat MatchData 像 array:
m[1] # => "Fayetteville"
m[3] # => "AR"
ปัญหา grouping ข้างบน: captured data ใน array ถ้า position เปลี่ยน ต้อง update code หรือ bug
เช่น decide capturing ", " silly ลบ parens ส่วนนั้น:
m = PLACE.match(/([^,]+), *([A-Z]{2})/)
# => #
m[3] # => nil
ตอนนี้ m[3] ไม่มี state - bug city
Named Groups
ทำ regex groups semantic มากขึ้นด้วยการ命名 syntax ใกล้เคียง ใส่ parens ระบุ name:
/(?regex)/
Apply กับ city/state regex:
m = PLACE.match(/(?[^,]+), *(?[A-Z]{2})/)
# => #
เข้าถึง captured data treat MatchData 像 hash:
m[:city] # => "Fayetteville"
Conditionals
Conditionals ใน regex รูปแบบ /(?(A)X|Y)/ วิธีใช้ valid:
# ถ้า A true evaluate X ไม่然 Y
/(?(A)X|Y)/
# ถ้า A true แล้ว X
/(?(A)X)/
# ถ้า A false แล้ว Y
/(?(A)|Y)/
Two common options สำหรับ condition A:
- Named/numbered group captured ไหม?
- Look-around evaluate true ไหม?
มาดูใช้งาน:
Group captured ไหม?
Check presence of group 用 ?(n) syntax n คือ integer หรือ group name ล้อม <> หรือ ''
# Group number 1 captured ไหม?
/(?(1)foo|bar)/
# Group 名 "mygroup" captured ไหม?
/(?()foo|bar)/
Example
จินตนาการ parse US telephone numbers มี three-digit area code optional เว้นแต่ number เริ่มด้วย 1
1-800-555-1212 # Valid
800-555-1212 # Valid
555-1212 # Valid
1-555-1212 # INVALID!!
用 conditional ทำ area code required ก็ต่อเมื่อเริ่มด้วย 1:
# Regex complex แต่ทำจาก simple pieces
# `^(1-)?` string เริ่ม "1-" ไหม ถ้า capture as group 1
# `(?(1)` Group one 有 capture ไหม?
# `\d{3}-` ถ้ามี required match 三位數和 dash (area code)
# `|(\d{3}-)?` ถ้าไม่มี optional match 三位數和 dash (area code)
# `\d{3}-\d{4}` match เหลือ phone number ที่ always required
re = /^(1-)?(?(1)\d{3}-|(\d{3}-)?)\d{3}-\d{4}/
"1-800-555-1212".match(re) #=> #
"800-555-1212".match(re) #=> #
"555-1212".match(re) #=> #
"1-555-1212".match(re) #=> nil
Limitations
ปัญหา group-based conditionals: matching group "consume" characters ใน string characters นั้นไม่สามารถ用 conditional ได้
เช่น code พยายาม match 100 ถ้า "USD" present:
"100USD".match(/(USD)(?(1)\d+)/) # nil
Perl และภาษาอื่นเพิ่ม look-ahead ใน conditional ให้ trigger conditional จาก text 任何位置 Ruby ไม่有 workaround creative
Look-around
โชคดี workaround limitations ใน Ruby regex conditionals ด้วยการ abuse look-around expressions
Look-around คืออะไร?
ปกติ regex parser step through string เริ่มถึงสิ้นสุดหา matches เหมือน move cursor ซ้ายขวาใน word processor
Look-ahead และ look-behind expressions ทำงานต่างออกไป inspect string 不消耗 characters 完了 cursor อยู่เดิม
Intro look-arounds ดีๆ: Rexegg guide to mastering look ahead and look behind
Syntax:
| Type | Syntax | Example |
|---|---|---|
| Look Ahead | (?=query) | \d+(?= dollars) match 100 ใน "100 dollars" |
| Negative Look Ahead | (?!query) | \d+(?! dollars) match 100 ถ้า NOT followed by "dollars" |
| Look Behind | (?<=query) | (?<=lucky )\d match 7 ใน "lucky 7" |
| Negative Look Behind | (? | (? match 7 在 "lucky 7" |
Abusing look-arounds 增强 conditionals
ใน conditional query ได้แค่ groups ที่ set แล้ว ปกติ group content consumed ไม่ available ให้ conditional
แต่用 look-ahead set group 不消耗 characters! mind blown?
จำ code ที่ไม่ work?
"100USD".match(/(USD)(?(1)\d+)/) # nil
修改 capture group ใน look-ahead ทำงานได้:
"100USD".match(/(?=.*(USD))(?(1)\d+)/)
# => #
แยก query ดู:
(?=.*(USD))用 look-ahead scan text หา "USD" capture group 1(?(1)ถ้า group 1 มีอยู่\d+แล้ว match หนึ่งหรือ多個 numbers
น่าสนใจไหม?