Files
mruby-mruby/mrbgems/mruby-regexp/test/regexp.rb
T
Yukihiro "Matz" Matsumoto 101f8c69a1 mruby-regexp: implement fixed-length lookbehind assertions
Add (?<=...) positive and (?<!...) negative lookbehind support.
The sub-pattern must have a fixed byte length (no quantifiers or
alternation), computed at compile time and stored in the instruction.
At execution time, the engine backs up by that many bytes and runs
the sub-pattern forward. Maximum lookbehind length is 255 bytes.

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 19:25:32 +09:00

428 lines
11 KiB
Ruby

assert("Regexp.new with string") do
re = Regexp.new("abc")
assert_kind_of Regexp, re
end
assert("Regexp.new with regexp") do
r1 = Regexp.new("abc", Regexp::IGNORECASE)
r2 = Regexp.new(r1)
assert_equal r1.source, r2.source
assert_equal r1.options, r2.options
assert_true r2.match?("ABC")
end
assert("Regexp#match - simple") do
re = Regexp.new("abc")
md = re.match("xabcy")
assert_kind_of MatchData, md
assert_equal "abc", md[0]
end
assert("Regexp#match - no match") do
re = Regexp.new("xyz")
assert_nil re.match("abc")
end
assert("Regexp#match?") do
re = Regexp.new("abc")
assert_true re.match?("xabcy")
assert_false re.match?("xyz")
end
assert("Regexp#=~") do
re = Regexp.new("bc")
assert_equal 1, re =~ "abcd"
assert_nil re =~ "xyz"
end
assert("Regexp#===") do
re = Regexp.new("abc")
assert_true re === "abc"
assert_false re === "xyz"
end
assert("Regexp - character class") do
re = Regexp.new("[a-z]+")
md = re.match("123abc456")
assert_equal "abc", md[0]
end
assert("Regexp - dot") do
re = Regexp.new("a.c")
assert_true re.match?("abc")
assert_true re.match?("axc")
assert_false re.match?("ac")
end
assert("Regexp - alternation") do
re = Regexp.new("cat|dog")
assert_equal "cat", re.match("I have a cat")[0]
assert_equal "dog", re.match("I have a dog")[0]
end
assert("Regexp - quantifiers") do
assert_equal "aaa", Regexp.new("a+").match("aaa")[0]
assert_equal "", Regexp.new("a*").match("bbb")[0]
assert_equal "ab", Regexp.new("ab?").match("ab")[0]
assert_equal "a", Regexp.new("ab?").match("ac")[0]
end
assert("Regexp - captures") do
re = Regexp.new("(\\w+)@(\\w+)")
md = re.match("user@host")
assert_equal "user@host", md[0]
assert_equal "user", md[1]
assert_equal "host", md[2]
end
assert("Regexp - \\d \\w \\s") do
assert_true Regexp.new("\\d+").match?("123")
assert_true Regexp.new("\\w+").match?("abc_123")
assert_true Regexp.new("\\s+").match?(" ")
assert_false Regexp.new("\\d+").match?("abc")
end
assert("Regexp - anchors") do
assert_true Regexp.new("^abc").match?("abc")
assert_false Regexp.new("^abc").match?("xabc")
assert_true Regexp.new("abc$").match?("abc")
assert_false Regexp.new("abc$").match?("abcx")
end
assert("Regexp - case insensitive") do
re = Regexp.new("abc", Regexp::IGNORECASE)
assert_true re.match?("ABC")
assert_true re.match?("Abc")
end
assert("Regexp - repetition {n,m}") do
assert_equal "aaa", Regexp.new("a{3}").match("aaaa")[0]
assert_equal "aa", Regexp.new("a{2,3}").match("aa")[0]
assert_equal "aaa", Regexp.new("a{2,3}").match("aaaa")[0]
end
assert("MatchData#captures") do
re = Regexp.new("(a)(b)(c)")
md = re.match("abc")
assert_equal ["a", "b", "c"], md.captures
end
assert("MatchData#pre_match / #post_match") do
re = Regexp.new("bc")
md = re.match("abcde")
assert_equal "a", md.pre_match
assert_equal "de", md.post_match
end
assert("MatchData#string") do
md = Regexp.new("bc").match("abcde")
assert_equal "abcde", md.string
end
assert("MatchData#regexp") do
re = Regexp.new("bc")
md = re.match("abcde")
assert_equal re, md.regexp
end
assert("MatchData#to_s") do
md = Regexp.new("bc").match("abcde")
assert_equal "bc", md.to_s
end
assert("MatchData#begin / #end") do
re = Regexp.new("bc")
md = re.match("abcde")
assert_equal 1, md.begin(0)
assert_equal 3, md.end(0)
end
assert("Regexp.escape") do
assert_equal "a\\.b\\*c", Regexp.escape("a.b*c")
end
assert("Regexp#inspect") do
re = Regexp.new("abc", Regexp::IGNORECASE)
assert_equal "/abc/i", re.inspect
end
assert("Regexp#to_s") do
assert_equal "(?:abc)", Regexp.new("abc").to_s
assert_equal "(?i:abc)", Regexp.new("abc", Regexp::IGNORECASE).to_s
assert_equal "(?m:abc)", Regexp.new("abc", Regexp::MULTILINE).to_s
assert_equal "(?im:abc)", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).to_s
end
assert("Regexp#== and Regexp#eql?") do
r1 = Regexp.new("abc", Regexp::IGNORECASE)
r2 = Regexp.new("abc", Regexp::IGNORECASE)
r3 = Regexp.new("abc")
r4 = Regexp.new("def", Regexp::IGNORECASE)
assert_true r1 == r2
assert_true r1.eql?(r2)
assert_false r1 == r3 # different flags
assert_false r1 == r4 # different source
assert_false r1 == "abc" # not a Regexp
end
assert("Regexp#hash") do
r1 = Regexp.new("abc", Regexp::IGNORECASE)
r2 = Regexp.new("abc", Regexp::IGNORECASE)
r3 = Regexp.new("abc")
assert_equal r1.hash, r2.hash
assert_not_equal r1.hash, r3.hash
end
assert("Regexp#options") do
assert_equal 0, Regexp.new("abc").options
assert_equal Regexp::IGNORECASE, Regexp.new("abc", Regexp::IGNORECASE).options
assert_equal Regexp::MULTILINE, Regexp.new("abc", Regexp::MULTILINE).options
assert_equal Regexp::EXTENDED, Regexp.new("abc", Regexp::EXTENDED).options
assert_equal Regexp::IGNORECASE | Regexp::MULTILINE,
Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).options
assert_equal Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE,
Regexp.new("abc", Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE).options
end
assert("Regexp#casefold?") do
assert_true Regexp.new("abc", Regexp::IGNORECASE).casefold?
assert_false Regexp.new("abc").casefold?
end
assert("Regexp extended mode (x flag)") do
# whitespace is ignored
re = Regexp.new('a b c', Regexp::EXTENDED)
assert_true re.match?("abc")
assert_false re.match?("a b c")
# comments are ignored
re = Regexp.new("a # match a\nb # match b\nc", Regexp::EXTENDED)
assert_true re.match?("abc")
# whitespace inside character class is literal
re = Regexp.new('[ ]', Regexp::EXTENDED)
assert_true re.match?(" ")
# escaped whitespace is preserved
re = Regexp.new('a\\ b', Regexp::EXTENDED)
assert_true re.match?("a b")
# inspect shows x flag
assert_equal "/abc/x", Regexp.new("abc", Regexp::EXTENDED).inspect
# to_s shows x flag
assert_equal "(?x:abc)", Regexp.new("abc", Regexp::EXTENDED).to_s
end
assert("String#match") do
md = "hello world".match(Regexp.new("(\\w+)\\s(\\w+)"))
assert_equal "hello", md[1]
assert_equal "world", md[2]
end
assert("String#sub") do
assert_equal "hXllo", "hello".sub(Regexp.new("e"), "X")
end
assert("String#gsub") do
assert_equal "h-ll-", "hello".gsub(Regexp.new("[eo]"), "-")
end
assert("String#sub with \\& \\` \\' specials") do
# \& = full match
assert_equal "a[bc]d", "abcd".sub(/bc/, '[\\&]')
# \` = pre_match
assert_equal "a[a]d", "abcd".sub(/bc/, '[\\`]')
# \' = post_match
assert_equal "a[d]d", "abcd".sub(/bc/, "[\\']")
# \+ = last capture
assert_equal "a[c]d", "abcd".sub(/(b)(c)/, '[\\+]')
# \\ = literal backslash
assert_equal "a\\d", "abcd".sub(/bc/, "\\\\")
# \1 still works
assert_equal "abbd", "abcd".sub(/(b)c/, '\\1\\1')
end
assert("String#gsub with \\& special") do
assert_equal "[a][b][c]", "abc".gsub(/./, '[\\&]')
end
assert("String#scan") do
assert_equal ["1", "2", "3"], "a1b2c3".scan(Regexp.new("\\d"))
end
assert("Regexp literal /regex/") do
assert_true /abc/.match?("abc")
assert_equal "123", /\d+/.match("abc123")[0]
assert_true /hello/i.match?("HELLO")
end
assert("$~ global variable") do
/(\w+)@(\w+)/ =~ "user@host"
assert_kind_of MatchData, $~
assert_equal "user", $~[1]
assert_equal "host", $~[2]
end
assert("$~ is nil on no match") do
/xyz/ =~ "abc"
assert_nil $~
end
assert("Regexp.last_match") do
/(\d+)/ =~ "abc123"
assert_equal "123", Regexp.last_match(1)
assert_equal "123", Regexp.last_match(0)
end
assert("Regexp - empty pattern") do
assert_true //.match?("")
assert_true //.match?("abc")
end
assert("Regexp - nested captures") do
md = /((a)(b))c/.match("abc")
assert_equal "abc", md[0]
assert_equal "ab", md[1]
assert_equal "a", md[2]
assert_equal "b", md[3]
end
assert("Regexp - non-greedy quantifiers") do
assert_equal "a", /a+?/.match("aaa")[0]
assert_equal "", /a*?/.match("aaa")[0]
end
assert("Regexp - word boundary") do
assert_equal "cat", /\bcat\b/.match("the cat sat")[0]
assert_nil /\bcat\b/.match("concatenate")
end
assert("Regexp - non-capturing group") do
md = /(?:a)(b)/.match("ab")
assert_equal "ab", md[0]
assert_equal "b", md[1]
assert_nil md[2]
end
assert("String#sub with block") do
assert_equal "HELLO world", "hello world".sub(/\w+/) { |m| m.upcase }
end
assert("String#gsub with block") do
assert_equal "HELLO WORLD", "hello world".gsub(/\w+/) { |m| m.upcase }
end
assert("String#gsub date reformat") do
result = "2026-03-21".gsub(/(\d+)-(\d+)-(\d+)/) { "#{$~[3]}/#{$~[2]}/#{$~[1]}" }
assert_equal "21/03/2026", result
end
assert("String#scan with captures") do
assert_equal [["1","a"],["2","b"]], "1a2b".scan(/(\d)(\w)/)
end
assert("String#split with regexp") do
assert_equal ["a", "b", "c"], "a, b, c".split(/,\s*/)
end
assert("Regexp - case in when") do
result = case "hello123"
when /\d+/ then "has digits"
else "no digits"
end
assert_equal "has digits", result
end
assert("Regexp - backreference \\1") do
# match repeated word
md = /(\w+) \1/.match("hello hello world")
assert_equal "hello hello", md[0]
assert_equal "hello", md[1]
end
assert("Regexp - backreference no match") do
assert_nil /(\w+) \1/.match("hello world")
end
assert("Regexp - named captures") do
md = /(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/.match("2026-03-21")
assert_equal "2026", md[:year]
assert_equal "03", md[:month]
assert_equal "21", md[:day]
assert_equal "2026", md["year"]
end
assert("MatchData#named_captures") do
md = /(?<a>\w+)@(?<b>\w+)/.match("user@host")
nc = md.named_captures
assert_equal "user", nc["a"]
assert_equal "host", nc["b"]
end
assert("Regexp - positive lookahead (?=...)") do
md = /\w+(?=@)/.match("user@host")
assert_equal "user", md[0]
end
assert("Regexp - negative lookahead (?!...)") do
md = /\d+(?!%)/.match("100%")
assert_equal "10", md[0]
end
assert("Regexp - lookahead does not consume") do
md = /foo(?=bar)/.match("foobar")
assert_equal "foo", md[0]
assert_nil /foo(?=baz)/.match("foobar")
end
assert("Regexp - positive lookbehind (?<=...)") do
md = Regexp.new("(?<=@)\\w+").match("user@host")
assert_equal "host", md[0]
assert_nil Regexp.new("(?<=@)\\w+").match("user_host")
end
assert("Regexp - negative lookbehind (?<!...)") do
md = Regexp.new("(?<!\\d)px").match("12px auto")
assert_nil md # preceded by digit
md = Regexp.new("(?<!\\d)em").match("12px 1.5em auto")
assert_nil md # preceded by digit
md = Regexp.new("(?<!\\d)px").match("top px")
assert_equal "px", md[0]
end
assert("Regexp - lookbehind with literal string") do
md = Regexp.new("(?<=foo)bar").match("foobar")
assert_equal "bar", md[0]
assert_nil Regexp.new("(?<=foo)bar").match("bazbar")
end
assert("Regexp - lookbehind at string start") do
# lookbehind should fail if not enough text before
assert_nil Regexp.new("(?<=abc)d").match("d")
# but should work at correct position
md = Regexp.new("(?<=abc)d").match("abcd")
assert_equal "d", md[0]
end
assert("Regexp - negative lookbehind at string start") do
# negative lookbehind succeeds when not enough text before
md = Regexp.new("(?<!x)a").match("a")
assert_equal "a", md[0]
end
assert("$1-$9 global variables") do
/(\w+)\s(\w+)/ =~ "hello world"
assert_equal "hello", $1
assert_equal "world", $2
assert_nil $3
end
assert("$1-$9 cleared on no match") do
/(\w+)/ =~ "hello"
assert_equal "hello", $1
/xyz/ =~ "abc"
assert_nil $1
end