I have a list of strings, and need to build the regular expression from them, using Regexp#union
. I need the resulting pattern to be case insensitive.
The #union
method itself does not accept options/modifiers, hence I currently see two options:
strings = %w|one two three|
Regexp.new(Regexp.union(strings).to_s, true)
and/or:
Regexp.union(*strings.map { |s| /#{s}/i })
Both variants look a bit weird.
Is there an ability to construct a case-insensitive regular expression by using Regexp.union
?
By default, the comparison of an input string with any literal characters in a regular expression pattern is case-sensitive, white space in a regular expression pattern is interpreted as literal white-space characters, and capturing groups in a regular expression are named implicitly as well as explicitly.
Basically (0+1)* mathes any sequence of ones and zeroes. So, in your example (0+1)*1(0+1)* should match any sequence that has 1. It would not match 000 , but it would match 010 , 1 , 111 etc. (0+1) means 0 OR 1. 1* means any number of ones.
The [] construct in a regex is essentially shorthand for an | on all of the contents. For example [abc] matches a, b or c. Additionally the - character has special meaning inside of a [] . It provides a range construct. The regex [a-z] will match any letter a through z.
The simple starting place is:
words = %w[one two three]
/#{ Regexp.union(words).source }/i # => /one|two|three/i
You probably want to make sure you're only matching words so tweak it to:
/\b#{ Regexp.union(words).source }\b/i # => /\bone|two|three\b/i
For cleanliness and clarity I prefer using a non-capturing group:
/\b(?:#{ Regexp.union(words).source })\b/i # => /\b(?:one|two|three)\b/i
Using source
is important. When you create a Regexp object, it has an idea of the flags (i
, m
, x
) that apply to that object and those get interpolated into the string:
"#{ /foo/i }" # => "(?i-mx:foo)"
"#{ /foo/ix }" # => "(?ix-m:foo)"
"#{ /foo/ixm }" # => "(?mix:foo)"
or
(/foo/i).to_s # => "(?i-mx:foo)"
(/foo/ix).to_s # => "(?ix-m:foo)"
(/foo/ixm).to_s # => "(?mix:foo)"
That's fine when the generated pattern stands alone, but when it's being interpolated into a string to define other parts of the pattern the flags affect each sub-expression:
/\b(?:#{ Regexp.union(words) })\b/i # => /\b(?:(?-mix:one|two|three))\b/i
Dig into the Regexp documentation and you'll see that ?-mix
turns off "ignore-case" inside (?-mix:one|two|three)
, even though the overall pattern is flagged with i
, resulting in a pattern that doesn't do what you want, and is really hard to debug:
'foo ONE bar'[/\b(?:#{ Regexp.union(words) })\b/i] # => nil
Instead, source
removes the inner expression's flags making the pattern do what you'd expect:
/\b(?:#{ Regexp.union(words).source })\b/i # => /\b(?:one|two|three)\b/i
and
'foo ONE bar'[/\b(?:#{ Regexp.union(words).source })\b/i] # => "ONE"
You can build your patterns using Regexp.new
and passing in the flags:
regexp = Regexp.new('(?:one|two|three)', Regexp::EXTENDED | Regexp::IGNORECASE) # => /(?:one|two|three)/ix
but as the expression becomes more complex it becomes unwieldy. Building a pattern using string interpolation remains more easy to understand.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With