From df6ed1955b0c5132f02da41c59ba366361705532 Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 10:47:15 +0100 Subject: [PATCH 01/14] feat(string, regex): implement #match! --- src/regex.cr | 15 +++++++++++++++ src/string.cr | 19 ++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/regex.cr b/src/regex.cr index 6c4109d62f91..6d413b2fbb95 100644 --- a/src/regex.cr +++ b/src/regex.cr @@ -530,6 +530,21 @@ class Regex match(str, pos, options: _options) end + # Matches a regular expression against `String` *str*. This starts at the character + # index *pos* if given, otherwise at the start of *str*. Returns a `Regex::MatchData` + # if *str* matched, otherwise raises an exception. `$~` will contain the same value + # if matched. + # + # ``` + # /(.)(.)(.)/.match!("abc")[2] # => "b" + # /(.)(.)/.match!("abc", 1)[2] # => "c" + # /(.)(タ)/.match!("クリスタル", 3)[2] # raises Exception + # ``` + def match!(str : String, pos : Int32 = 0, *, options : Regex::MatchOptions = :none) : MatchData + byte_index = str.char_index_to_byte_index(pos) || raise "no matches found" + $~ = match_at_byte_index(str, byte_index, options) || raise "no matches found" + end + # Match at byte index. Matches a regular expression against `String` # *str*. Starts at the byte index given by *pos* if given, otherwise at # the start of *str*. Returns a `Regex::MatchData` if *str* matched, otherwise diff --git a/src/string.cr b/src/string.cr index 7d446d2edd34..7cd11356a845 100644 --- a/src/string.cr +++ b/src/string.cr @@ -4540,8 +4540,7 @@ class String end end - # Finds match of *regex*, starting at *pos*. - # It also updates `$~` with the result. + # Finds matches of *regex* starting at *pos* and updates `$~` to the result. # # ``` # "foo".match(/foo/) # => Regex::MatchData("foo") @@ -4551,9 +4550,19 @@ class String # $~ # raises Exception # ``` def match(regex : Regex, pos = 0) : Regex::MatchData? - match = regex.match self, pos - $~ = match - match + $~ = regex.match self, pos + end + + # Finds matches of *regex* starting at *pos* and updates `$~` to the result. + # This will raise an exception if there are no matches. + # + # ``` + # "foo".match!(/foo/) # => Regex::MatchData("foo") + # $~ # => Regex::MatchData("foo") + # + # "foo".match!(/bar/) # => raises Exception + def match!(regex : Regex, pos = 0) : Regex::MatchData + $~ = regex.match! self, pos end # Finds match of *regex* like `#match`, but it returns `Bool` value. From e8614e7abd1b39decd51ca1f0327f9574dc2f310 Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 10:48:07 +0100 Subject: [PATCH 02/14] chore(spec/std): add Regex#match! specs --- spec/std/regex_spec.cr | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/spec/std/regex_spec.cr b/spec/std/regex_spec.cr index f71684ef66bb..3f76d48a4656 100644 --- a/spec/std/regex_spec.cr +++ b/spec/std/regex_spec.cr @@ -124,6 +124,35 @@ describe "Regex" do end end + describe "#match!" do + it "returns match data" do + md = /(?.)(?.)/.match!("Crystal") + md[0].should eq "Cr" + md.captures.should eq [] of String + md.named_captures.should eq({"bar" => "C", "foo" => "r"}) + end + + it "assigns captures" do + md = /foo/.match!("foo") + $~.should eq md + end + + it "raises on non-match" do + expect_raises { /Crystal/.match!("foo") } + expect_raises(NilAssertionError) { $~ } + end + + context "with options" do + it "deprecated Regex::Options" do + expect_raises { /foo/.match!(".foo", options: Regex::Options::ANCHORED) } + end + + it "Regex::Match options" do + expect_raises { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } + end + end + end + describe "#match_at_byte_index" do it "assigns captures" do matchdata = /foo/.match_at_byte_index("..foo", 1) From 9d8e3d79703d4b08d3d4a4c0a06616110db1edda Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 10:51:31 +0100 Subject: [PATCH 03/14] fix(spec/std): add exception arguments --- spec/std/regex_spec.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/std/regex_spec.cr b/spec/std/regex_spec.cr index 3f76d48a4656..3ee262c59e8f 100644 --- a/spec/std/regex_spec.cr +++ b/spec/std/regex_spec.cr @@ -138,17 +138,17 @@ describe "Regex" do end it "raises on non-match" do - expect_raises { /Crystal/.match!("foo") } + expect_raises(Exception) { /Crystal/.match!("foo") } expect_raises(NilAssertionError) { $~ } end context "with options" do it "deprecated Regex::Options" do - expect_raises { /foo/.match!(".foo", options: Regex::Options::ANCHORED) } + expect_raises(Exception) { /foo/.match!(".foo", options: Regex::Options::ANCHORED) } end it "Regex::Match options" do - expect_raises { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } + expect_raises(Exception) { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } end end end From 31661d59225ffd43eb0821a31dcf9a449480636a Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 10:55:24 +0100 Subject: [PATCH 04/14] chore(spec/std): remove deprecated regex match spec --- spec/std/regex_spec.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/std/regex_spec.cr b/spec/std/regex_spec.cr index 3ee262c59e8f..3e1fcb58b247 100644 --- a/spec/std/regex_spec.cr +++ b/spec/std/regex_spec.cr @@ -143,10 +143,6 @@ describe "Regex" do end context "with options" do - it "deprecated Regex::Options" do - expect_raises(Exception) { /foo/.match!(".foo", options: Regex::Options::ANCHORED) } - end - it "Regex::Match options" do expect_raises(Exception) { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } end From 5048780c1c53cbf95fe26f4d3d4757d5252da23d Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 11:14:54 +0100 Subject: [PATCH 05/14] feat(regex/match_data): replace not_nil! examples with match! --- src/regex/match_data.cr | 98 ++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/regex/match_data.cr b/src/regex/match_data.cr index 949bce29e603..b984f7fba099 100644 --- a/src/regex/match_data.cr +++ b/src/regex/match_data.cr @@ -21,32 +21,32 @@ class Regex # Returns the original regular expression. # # ``` - # "Crystal".match(/[p-s]/).not_nil!.regex # => /[p-s]/ + # "Crystal".match!(/[p-s]/).regex # => /[p-s]/ # ``` getter regex : Regex # Returns the number of capture groups, including named capture groups. # # ``` - # "Crystal".match(/[p-s]/).not_nil!.group_size # => 0 - # "Crystal".match(/r(ys)/).not_nil!.group_size # => 1 - # "Crystal".match(/r(ys)(?ta)/).not_nil!.group_size # => 2 + # "Crystal".match!(/[p-s]/).group_size # => 0 + # "Crystal".match!(/r(ys)/).group_size # => 1 + # "Crystal".match!(/r(ys)(?ta)/).group_size # => 2 # ``` getter group_size : Int32 # Returns the original string. # # ``` - # "Crystal".match(/[p-s]/).not_nil!.string # => "Crystal" + # "Crystal".match!(/[p-s]/).string # => "Crystal" # ``` getter string : String # Returns the number of elements in this match object. # # ``` - # "Crystal".match(/[p-s]/).not_nil!.size # => 1 - # "Crystal".match(/r(ys)/).not_nil!.size # => 2 - # "Crystal".match(/r(ys)(?ta)/).not_nil!.size # => 3 + # "Crystal".match!(/[p-s]/).size # => 1 + # "Crystal".match!(/r(ys)/).size # => 2 + # "Crystal".match!(/r(ys)(?ta)/).size # => 3 # ``` def size : Int32 group_size + 1 @@ -61,11 +61,11 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match(/r/).not_nil!.begin(0) # => 1 - # "Crystal".match(/r(ys)/).not_nil!.begin(1) # => 2 - # "クリスタル".match(/リ(ス)/).not_nil!.begin(0) # => 1 - # "Crystal".match(/r/).not_nil!.begin(1) # IndexError: Invalid capture group index: 1 - # "Crystal".match(/r(x)?/).not_nil!.begin(1) # IndexError: Capture group 1 was not matched + # "Crystal".match!(/r/).begin(0) # => 1 + # "Crystal".match!(/r(ys)/).begin(1) # => 2 + # "クリスタル".match!(/リ(ス)/).begin(0) # => 1 + # "Crystal".match!(/r/).begin(1) # IndexError: Invalid capture group index: 1 + # "Crystal".match!(/r(x)?/).begin(1) # IndexError: Capture group 1 was not matched # ``` def begin(n = 0) : Int32 @string.byte_index_to_char_index(byte_begin(n)).not_nil! @@ -80,11 +80,11 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match(/r/).not_nil!.end(0) # => 2 - # "Crystal".match(/r(ys)/).not_nil!.end(1) # => 4 - # "クリスタル".match(/リ(ス)/).not_nil!.end(0) # => 3 - # "Crystal".match(/r/).not_nil!.end(1) # IndexError: Invalid capture group index: 1 - # "Crystal".match(/r(x)?/).not_nil!.end(1) # IndexError: Capture group 1 was not matched + # "Crystal".match!(/r/).end(0) # => 2 + # "Crystal".match!(/r(ys)/).end(1) # => 4 + # "クリスタル".match!(/リ(ス)/).end(0) # => 3 + # "Crystal".match!(/r/).end(1) # IndexError: Invalid capture group index: 1 + # "Crystal".match!(/r(x)?/).end(1) # IndexError: Capture group 1 was not matched # ``` def end(n = 0) : Int32 @string.byte_index_to_char_index(byte_end(n)).not_nil! @@ -99,11 +99,11 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match(/r/).not_nil!.byte_begin(0) # => 1 - # "Crystal".match(/r(ys)/).not_nil!.byte_begin(1) # => 2 - # "クリスタル".match(/リ(ス)/).not_nil!.byte_begin(0) # => 3 - # "Crystal".match(/r/).not_nil!.byte_begin(1) # IndexError: Invalid capture group index: 1 - # "Crystal".match(/r(x)?/).not_nil!.byte_begin(1) # IndexError: Capture group 1 was not matched + # "Crystal".match!(/r/).byte_begin(0) # => 1 + # "Crystal".match!(/r(ys)/).byte_begin(1) # => 2 + # "クリスタル".match!(/リ(ス)/).byte_begin(0) # => 3 + # "Crystal".match!(/r/).byte_begin(1) # IndexError: Invalid capture group index: 1 + # "Crystal".match!(/r(x)?/).byte_begin(1) # IndexError: Capture group 1 was not matched # ``` def byte_begin(n = 0) : Int32 check_index_out_of_bounds n @@ -119,11 +119,11 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match(/r/).not_nil!.byte_end(0) # => 2 - # "Crystal".match(/r(ys)/).not_nil!.byte_end(1) # => 4 - # "クリスタル".match(/リ(ス)/).not_nil!.byte_end(0) # => 9 - # "Crystal".match(/r/).not_nil!.byte_end(1) # IndexError: Invalid capture group index: 1 - # "Crystal".match(/r(x)?/).not_nil!.byte_end(1) # IndexError: Capture group 1 was not matched + # "Crystal".match!(/r/).byte_end(0) # => 2 + # "Crystal".match!(/r(ys)/).byte_end(1) # => 4 + # "クリスタル".match!(/リ(ス)/).byte_end(0) # => 9 + # "Crystal".match!(/r/).byte_end(1) # IndexError: Invalid capture group index: 1 + # "Crystal".match!(/r(x)?/).byte_end(1) # IndexError: Capture group 1 was not matched # ``` def byte_end(n = 0) : Int32 check_index_out_of_bounds n @@ -136,9 +136,9 @@ class Regex # When *n* is `0`, returns the match for the entire `Regex`. # # ``` - # "Crystal".match(/r(ys)/).not_nil![0]? # => "rys" - # "Crystal".match(/r(ys)/).not_nil![1]? # => "ys" - # "Crystal".match(/r(ys)/).not_nil![2]? # => nil + # "Crystal".match!(/r(ys)/)[0]? # => "rys" + # "Crystal".match!(/r(ys)/)[1]? # => "ys" + # "Crystal".match!(/r(ys)/)[2]? # => nil # ``` def []?(n : Int) : String? return unless valid_group?(n) @@ -151,8 +151,8 @@ class Regex # if there is no *n*th capture group. # # ``` - # "Crystal".match(/r(ys)/).not_nil![1] # => "ys" - # "Crystal".match(/r(ys)/).not_nil![2] # raises IndexError + # "Crystal".match!(/r(ys)/)[1] # => "ys" + # "Crystal".match!(/r(ys)/)[2] # raises IndexError # ``` def [](n : Int) : String check_index_out_of_bounds n @@ -165,15 +165,15 @@ class Regex # `nil` if there is no such named capture group. # # ``` - # "Crystal".match(/r(?ys)/).not_nil!["ok"]? # => "ys" - # "Crystal".match(/r(?ys)/).not_nil!["ng"]? # => nil + # "Crystal".match!(/r(?ys)/)["ok"]? # => "ys" + # "Crystal".match!(/r(?ys)/)["ng"]? # => nil # ``` # # When there are capture groups having same name, it returns the last # matched capture group. # # ``` - # "Crystal".match(/(?Cr).*(?al)/).not_nil!["ok"]? # => "al" + # "Crystal".match!(/(?Cr).*(?al)/)["ok"]? # => "al" # ``` def []?(group_name : String) : String? fetch_impl(group_name) { nil } @@ -183,15 +183,15 @@ class Regex # raises an `KeyError` if there is no such named capture group. # # ``` - # "Crystal".match(/r(?ys)/).not_nil!["ok"] # => "ys" - # "Crystal".match(/r(?ys)/).not_nil!["ng"] # raises KeyError + # "Crystal".match!(/r(?ys)/)["ok"] # => "ys" + # "Crystal".match!(/r(?ys)/)["ng"] # raises KeyError # ``` # # When there are capture groups having same name, it returns the last # matched capture group. # # ``` - # "Crystal".match(/(?Cr).*(?al)/).not_nil!["ok"] # => "al" + # "Crystal".match!(/(?Cr).*(?al)/)["ok"] # => "al" # ``` def [](group_name : String) : String fetch_impl(group_name) { |exists| @@ -230,7 +230,7 @@ class Regex # starts at the start of the string, returns the empty string. # # ``` - # "Crystal".match(/yst/).not_nil!.pre_match # => "Cr" + # "Crystal".match!(/yst/).pre_match # => "Cr" # ``` def pre_match : String @string.byte_slice(0, byte_begin(0)) @@ -240,7 +240,7 @@ class Regex # at the end of the string, returns the empty string. # # ``` - # "Crystal".match(/yst/).not_nil!.post_match # => "al" + # "Crystal".match!(/yst/).post_match # => "al" # ``` def post_match : String @string.byte_slice(byte_end(0)) @@ -251,12 +251,12 @@ class Regex # It is a difference from `to_a` that the result array does not contain the match for the entire `Regex` (`self[0]`). # # ``` - # match = "Crystal".match(/(Cr)(?y)(st)(?al)/).not_nil! + # match = "Crystal".match!(/(Cr)(?y)(st)(?al)/) # match.captures # => ["Cr", "st"] # # # When this regex has an optional group, result array may contain # # a `nil` if this group is not matched. - # match = "Crystal".match(/(Cr)(stal)?/).not_nil! + # match = "Crystal".match!(/(Cr)(stal)?/) # match.captures # => ["Cr", nil] # ``` def captures : Array(String?) @@ -273,12 +273,12 @@ class Regex # Returns a hash of named capture groups. # # ``` - # match = "Crystal".match(/(Cr)(?y)(st)(?al)/).not_nil! + # match = "Crystal".match!(/(Cr)(?y)(st)(?al)/) # match.named_captures # => {"name1" => "y", "name2" => "al"} # # # When this regex has an optional group, result hash may contain # # a `nil` if this group is not matched. - # match = "Crystal".match(/(?Cr)(?stal)?/).not_nil! + # match = "Crystal".match!(/(?Cr)(?stal)?/) # match.named_captures # => {"name1" => "Cr", "name2" => nil} # ``` def named_captures : Hash(String, String?) @@ -297,12 +297,12 @@ class Regex # Convert this match data into an array. # # ``` - # match = "Crystal".match(/(Cr)(?y)(st)(?al)/).not_nil! + # match = "Crystal".match!(/(Cr)(?y)(st)(?al)/) # match.to_a # => ["Crystal", "Cr", "y", "st", "al"] # # # When this regex has an optional group, result array may contain # # a `nil` if this group is not matched. - # match = "Crystal".match(/(Cr)(?stal)?/).not_nil! + # match = "Crystal".match!(/(Cr)(?stal)?/) # match.to_a # => ["Cr", "Cr", nil] # ``` def to_a : Array(String?) @@ -312,12 +312,12 @@ class Regex # Convert this match data into a hash. # # ``` - # match = "Crystal".match(/(Cr)(?y)(st)(?al)/).not_nil! + # match = "Crystal".match!(/(Cr)(?y)(st)(?al)/) # match.to_h # => {0 => "Crystal", 1 => "Cr", "name1" => "y", 3 => "st", "name2" => "al"} # # # When this regex has an optional group, result array may contain # # a `nil` if this group is not matched. - # match = "Crystal".match(/(Cr)(?stal)?/).not_nil! + # match = "Crystal".match!(/(Cr)(?stal)?/) # match.to_h # => {0 => "Cr", 1 => "Cr", "name1" => nil} # ``` def to_h : Hash(Int32 | String, String?) From 0a5d200183f2a752bbbed167c60c47fd335952e3 Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 12:15:22 +0100 Subject: [PATCH 06/14] chore(regex): use Error over Exception --- spec/std/regex_spec.cr | 4 ++-- src/regex.cr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/std/regex_spec.cr b/spec/std/regex_spec.cr index 3e1fcb58b247..ccf3b49eddbc 100644 --- a/spec/std/regex_spec.cr +++ b/spec/std/regex_spec.cr @@ -138,13 +138,13 @@ describe "Regex" do end it "raises on non-match" do - expect_raises(Exception) { /Crystal/.match!("foo") } + expect_raises(Regex::Error) { /Crystal/.match!("foo") } expect_raises(NilAssertionError) { $~ } end context "with options" do it "Regex::Match options" do - expect_raises(Exception) { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } + expect_raises(Regex::Error) { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } end end end diff --git a/src/regex.cr b/src/regex.cr index 6d413b2fbb95..9fdd5ff34d90 100644 --- a/src/regex.cr +++ b/src/regex.cr @@ -541,8 +541,8 @@ class Regex # /(.)(タ)/.match!("クリスタル", 3)[2] # raises Exception # ``` def match!(str : String, pos : Int32 = 0, *, options : Regex::MatchOptions = :none) : MatchData - byte_index = str.char_index_to_byte_index(pos) || raise "no matches found" - $~ = match_at_byte_index(str, byte_index, options) || raise "no matches found" + byte_index = str.char_index_to_byte_index(pos) || raise Error.new "no matches found" + $~ = match_at_byte_index(str, byte_index, options) || raise Error.new "no matches found" end # Match at byte index. Matches a regular expression against `String` From 15e018c225266054f0ee13dbc39dbc7d3bba2251 Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 12:40:20 +0100 Subject: [PATCH 07/14] chore(regex): improve error message --- src/regex.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/regex.cr b/src/regex.cr index 9fdd5ff34d90..1b5324782f2b 100644 --- a/src/regex.cr +++ b/src/regex.cr @@ -541,8 +541,8 @@ class Regex # /(.)(タ)/.match!("クリスタル", 3)[2] # raises Exception # ``` def match!(str : String, pos : Int32 = 0, *, options : Regex::MatchOptions = :none) : MatchData - byte_index = str.char_index_to_byte_index(pos) || raise Error.new "no matches found" - $~ = match_at_byte_index(str, byte_index, options) || raise Error.new "no matches found" + byte_index = str.char_index_to_byte_index(pos) || raise Error.new "Match not found" + $~ = match_at_byte_index(str, byte_index, options) || raise Error.new "Match not found" end # Match at byte index. Matches a regular expression against `String` From a796411aa6a6308623a86fcfd4e60c6c1d1748e4 Mon Sep 17 00:00:00 2001 From: Devonte W Date: Wed, 5 Apr 2023 13:56:17 +0100 Subject: [PATCH 08/14] Update spec/std/regex_spec.cr Co-authored-by: Sijawusz Pur Rahnama --- spec/std/regex_spec.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/std/regex_spec.cr b/spec/std/regex_spec.cr index ccf3b49eddbc..c07127b3bd09 100644 --- a/spec/std/regex_spec.cr +++ b/spec/std/regex_spec.cr @@ -138,7 +138,7 @@ describe "Regex" do end it "raises on non-match" do - expect_raises(Regex::Error) { /Crystal/.match!("foo") } + expect_raises(Regex::Error, "Match not found") { /Crystal/.match!("foo") } expect_raises(NilAssertionError) { $~ } end From a823ae0c9c5b231444ccea8777e0007223930cd9 Mon Sep 17 00:00:00 2001 From: Devonte W Date: Wed, 5 Apr 2023 13:56:23 +0100 Subject: [PATCH 09/14] Update spec/std/regex_spec.cr Co-authored-by: Sijawusz Pur Rahnama --- spec/std/regex_spec.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/std/regex_spec.cr b/spec/std/regex_spec.cr index c07127b3bd09..7446dc631911 100644 --- a/spec/std/regex_spec.cr +++ b/spec/std/regex_spec.cr @@ -144,7 +144,9 @@ describe "Regex" do context "with options" do it "Regex::Match options" do - expect_raises(Regex::Error) { /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) } + expect_raises(Regex::Error, "Match not found") do + /foo/.match!(".foo", options: Regex::MatchOptions::ANCHORED) + end end end end From f150c535ce685d075c266de80ef1210041730852 Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Wed, 5 Apr 2023 23:55:37 +0100 Subject: [PATCH 10/14] chore(regex): formatting --- src/regex.cr | 4 ++-- src/regex/match_data.cr | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/regex.cr b/src/regex.cr index 1b5324782f2b..c881d3850215 100644 --- a/src/regex.cr +++ b/src/regex.cr @@ -536,8 +536,8 @@ class Regex # if matched. # # ``` - # /(.)(.)(.)/.match!("abc")[2] # => "b" - # /(.)(.)/.match!("abc", 1)[2] # => "c" + # /(.)(.)(.)/.match!("abc")[2] # => "b" + # /(.)(.)/.match!("abc", 1)[2] # => "c" # /(.)(タ)/.match!("クリスタル", 3)[2] # raises Exception # ``` def match!(str : String, pos : Int32 = 0, *, options : Regex::MatchOptions = :none) : MatchData diff --git a/src/regex/match_data.cr b/src/regex/match_data.cr index b984f7fba099..b9271d423f82 100644 --- a/src/regex/match_data.cr +++ b/src/regex/match_data.cr @@ -61,9 +61,9 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match!(/r/).begin(0) # => 1 - # "Crystal".match!(/r(ys)/).begin(1) # => 2 - # "クリスタル".match!(/リ(ス)/).begin(0) # => 1 + # "Crystal".match!(/r/).begin(0) # => 1 + # "Crystal".match!(/r(ys)/).begin(1) # => 2 + # "クリスタル".match!(/リ(ス)/).begin(0) # => 1 # "Crystal".match!(/r/).begin(1) # IndexError: Invalid capture group index: 1 # "Crystal".match!(/r(x)?/).begin(1) # IndexError: Capture group 1 was not matched # ``` @@ -80,9 +80,9 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match!(/r/).end(0) # => 2 - # "Crystal".match!(/r(ys)/).end(1) # => 4 - # "クリスタル".match!(/リ(ス)/).end(0) # => 3 + # "Crystal".match!(/r/).end(0) # => 2 + # "Crystal".match!(/r(ys)/).end(1) # => 4 + # "クリスタル".match!(/リ(ス)/).end(0) # => 3 # "Crystal".match!(/r/).end(1) # IndexError: Invalid capture group index: 1 # "Crystal".match!(/r(x)?/).end(1) # IndexError: Capture group 1 was not matched # ``` @@ -99,9 +99,9 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match!(/r/).byte_begin(0) # => 1 - # "Crystal".match!(/r(ys)/).byte_begin(1) # => 2 - # "クリスタル".match!(/リ(ス)/).byte_begin(0) # => 3 + # "Crystal".match!(/r/).byte_begin(0) # => 1 + # "Crystal".match!(/r(ys)/).byte_begin(1) # => 2 + # "クリスタル".match!(/リ(ス)/).byte_begin(0) # => 3 # "Crystal".match!(/r/).byte_begin(1) # IndexError: Invalid capture group index: 1 # "Crystal".match!(/r(x)?/).byte_begin(1) # IndexError: Capture group 1 was not matched # ``` @@ -119,9 +119,9 @@ class Regex # subpattern is unused. # # ``` - # "Crystal".match!(/r/).byte_end(0) # => 2 - # "Crystal".match!(/r(ys)/).byte_end(1) # => 4 - # "クリスタル".match!(/リ(ス)/).byte_end(0) # => 9 + # "Crystal".match!(/r/).byte_end(0) # => 2 + # "Crystal".match!(/r(ys)/).byte_end(1) # => 4 + # "クリスタル".match!(/リ(ス)/).byte_end(0) # => 9 # "Crystal".match!(/r/).byte_end(1) # IndexError: Invalid capture group index: 1 # "Crystal".match!(/r(x)?/).byte_end(1) # IndexError: Capture group 1 was not matched # ``` From d0d7e5edf83f272268203b2bfba996e3eaa56158 Mon Sep 17 00:00:00 2001 From: Devonte W Date: Fri, 21 Apr 2023 11:06:23 +0100 Subject: [PATCH 11/14] fix: remove unnecessary typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- src/regex.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/regex.cr b/src/regex.cr index b42f6f4b9260..3e1bd7a53961 100644 --- a/src/regex.cr +++ b/src/regex.cr @@ -557,7 +557,7 @@ class Regex match(str, pos, options: _options) end - # Matches a regular expression against `String` *str*. This starts at the character + # Matches a regular expression against *str*. This starts at the character # index *pos* if given, otherwise at the start of *str*. Returns a `Regex::MatchData` # if *str* matched, otherwise raises an exception. `$~` will contain the same value # if matched. From 5070d5bf876c5b6510d83350da8bc205f8686f34 Mon Sep 17 00:00:00 2001 From: Devonte W Date: Fri, 21 Apr 2023 11:06:51 +0100 Subject: [PATCH 12/14] fix: include exception type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- src/regex.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/regex.cr b/src/regex.cr index 3e1bd7a53961..74aa17912925 100644 --- a/src/regex.cr +++ b/src/regex.cr @@ -559,7 +559,7 @@ class Regex # Matches a regular expression against *str*. This starts at the character # index *pos* if given, otherwise at the start of *str*. Returns a `Regex::MatchData` - # if *str* matched, otherwise raises an exception. `$~` will contain the same value + # if *str* matched, otherwise raises `Regex::Error`. `$~` will contain the same value # if matched. # # ``` From 7554209228d2317fcc5fe0248f996fe7a4a152b8 Mon Sep 17 00:00:00 2001 From: Devonte W Date: Fri, 21 Apr 2023 11:07:40 +0100 Subject: [PATCH 13/14] fix: include exception type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Müller --- src/string.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/string.cr b/src/string.cr index 6cdee93bb2c4..c9c03718226d 100644 --- a/src/string.cr +++ b/src/string.cr @@ -4561,7 +4561,7 @@ class String end # Finds matches of *regex* starting at *pos* and updates `$~` to the result. - # This will raise an exception if there are no matches. + # Raises `Regex::Error` if there are no matches. # # ``` # "foo".match!(/foo/) # => Regex::MatchData("foo") From 1730fa9c9071556caacb0064cae67b732b3be98d Mon Sep 17 00:00:00 2001 From: devnote-dev Date: Fri, 21 Apr 2023 19:24:45 +0100 Subject: [PATCH 14/14] chore(spec/std): add String#match! specs --- spec/std/string_spec.cr | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/spec/std/string_spec.cr b/spec/std/string_spec.cr index b988c55fe719..6e047c0d5570 100644 --- a/spec/std/string_spec.cr +++ b/spec/std/string_spec.cr @@ -2464,6 +2464,25 @@ describe "String" do end end + describe "match!" do + it "returns matchdata" do + md = "Crystal".match! /(?.)(?.)/ + md[0].should eq "Cr" + md.captures.should eq [] of String + md.named_captures.should eq({"bar" => "C", "foo" => "r"}) + end + + it "assigns captures" do + md = "foo".match! /foo/ + $~.should eq md + end + + it "raises on non-match" do + expect_raises(Regex::Error, "Match not found") { "foo".match! /Crystal/ } + expect_raises(NilAssertionError) { $~ } + end + end + it "does %" do ("Hello %d world" % 123).should eq("Hello 123 world") ("Hello %d world" % [123]).should eq("Hello 123 world")