Skip to content

Commit a2ab12a

Browse files
authored
Add StringLiteral#match (#16464)
1 parent 815101d commit a2ab12a

File tree

3 files changed

+59
-28
lines changed

3 files changed

+59
-28
lines changed

spec/compiler/macro/macro_methods_spec.cr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,12 @@ module Crystal
598598
assert_macro %q({{ "bar".gsub /(foo)/ { "STR" } }}), %("bar") # No match at all
599599
end
600600

601+
it "executes match" do
602+
assert_macro %({{ "hello world".match(/x/) }}), %(nil)
603+
assert_macro %({{ "hello world".match(/o.*o/) }}), %({0 => "o wo"} of ::Int32 | ::String => ::String | ::Nil)
604+
assert_macro %({{ "hello world".match(/(?:(x)|e)(?<name>\\S+)/) }}), %({0 => "ello", 1 => nil, "name" => "llo"} of ::Int32 | ::String => ::String | ::Nil)
605+
end
606+
601607
it "executes scan" do
602608
assert_macro %({{"Crystal".scan(/(Cr)(?<name1>y)(st)(?<name2>al)/)}}), %([{0 => "Crystal", 1 => "Cr", "name1" => "y", 3 => "st", "name2" => "al"} of ::Int32 | ::String => ::String | ::Nil] of ::Hash(::Int32 | ::String, ::String | ::Nil))
603609
assert_macro %({{"Crystal".scan(/(Cr)?(stal)/)}}), %([{0 => "stal", 1 => nil, 2 => "stal"} of ::Int32 | ::String => ::String | ::Nil] of ::Hash(::Int32 | ::String, ::String | ::Nil))

src/compiler/crystal/macros.cr

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ private macro def_string_methods(klass)
1414
def [](range : RangeLiteral) : {{klass}}
1515
end
1616

17-
# Similar to `String#=~`.
17+
# Similar to `String#matches?`.
1818
def =~(range : RegexLiteral) : BoolLiteral
1919
end
2020

@@ -68,10 +68,17 @@ private macro def_string_methods(klass)
6868
def includes?(search : StringLiteral | CharLiteral) : BoolLiteral
6969
end
7070

71+
# Matches the given *regex* against this string and returns a capture hash, or
72+
# `nil` if a match cannot be found.
73+
#
74+
# The capture hash has the same form as `Regex::MatchData#to_h`.
75+
def match(regex : RegexLiteral) : HashLiteral(NumberLiteral | StringLiteral, StringLiteral | NilLiteral)
76+
end
77+
7178
# Returns an array of capture hashes for each match of *regex* in this string.
7279
#
7380
# Capture hashes have the same form as `Regex::MatchData#to_h`.
74-
def scan(regex : RegexLiteral) : ArrayLiteral(HashLiteral(NumberLiteral | StringLiteral), StringLiteral | NilLiteral)
81+
def scan(regex : RegexLiteral) : ArrayLiteral(HashLiteral(NumberLiteral | StringLiteral, StringLiteral | NilLiteral))
7582
end
7683

7784
# Similar to `String#size`.

src/compiler/crystal/macros/methods.cr

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,20 @@ module Crystal
791791
end
792792
BoolLiteral.new(@value.includes?(piece))
793793
end
794+
when "match"
795+
interpret_check_args do |arg|
796+
unless arg.is_a?(RegexLiteral)
797+
raise "StringLiteral#match expects a regex, not #{arg.class_desc}"
798+
end
799+
800+
regex = regex_value(arg)
801+
802+
if match_data = @value.match(regex)
803+
regex_captures_hash(match_data)
804+
else
805+
NilLiteral.new
806+
end
807+
end
794808
when "scan"
795809
interpret_check_args do |arg|
796810
unless arg.is_a?(RegexLiteral)
@@ -810,32 +824,7 @@ module Crystal
810824
)
811825

812826
@value.scan(regex) do |match_data|
813-
captures = HashLiteral.new(
814-
of: HashLiteral::Entry.new(
815-
Union.new([Path.global("Int32"), Path.global("String")] of ASTNode),
816-
Union.new([Path.global("String"), Path.global("Nil")] of ASTNode),
817-
)
818-
)
819-
820-
match_data.to_h.each do |capture, substr|
821-
case capture
822-
in Int32
823-
key = NumberLiteral.new(capture)
824-
in String
825-
key = StringLiteral.new(capture)
826-
end
827-
828-
case substr
829-
in String
830-
value = StringLiteral.new(substr)
831-
in Nil
832-
value = NilLiteral.new
833-
end
834-
835-
captures.entries << HashLiteral::Entry.new(key, value)
836-
end
837-
838-
matches.elements << captures
827+
matches.elements << regex_captures_hash(match_data)
839828
end
840829

841830
matches
@@ -3381,6 +3370,35 @@ private def empty_no_return_array
33813370
Crystal::ArrayLiteral.new(of: Crystal::Path.global("NoReturn"))
33823371
end
33833372

3373+
private def regex_captures_hash(match_data : Regex::MatchData)
3374+
captures = Crystal::HashLiteral.new(
3375+
of: Crystal::HashLiteral::Entry.new(
3376+
Crystal::Union.new([Crystal::Path.global("Int32"), Crystal::Path.global("String")] of Crystal::ASTNode),
3377+
Crystal::Union.new([Crystal::Path.global("String"), Crystal::Path.global("Nil")] of Crystal::ASTNode),
3378+
)
3379+
)
3380+
3381+
match_data.to_h.each do |capture, substr|
3382+
case capture
3383+
in Int32
3384+
key = Crystal::NumberLiteral.new(capture)
3385+
in String
3386+
key = Crystal::StringLiteral.new(capture)
3387+
end
3388+
3389+
case substr
3390+
in String
3391+
value = Crystal::StringLiteral.new(substr)
3392+
in Nil
3393+
value = Crystal::NilLiteral.new
3394+
end
3395+
3396+
captures.entries << Crystal::HashLiteral::Entry.new(key, value)
3397+
end
3398+
3399+
captures
3400+
end
3401+
33843402
private def filter(object, klass, block, interpreter, keep = true)
33853403
block_arg = block.args.first?
33863404

0 commit comments

Comments
 (0)