fast_regexp 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +13 -1
- data/ext/fast_regexp/src/lib.rs +285 -154
- data/lib/fast_regexp/version.rb +1 -1
- data/lib/fast_regexp.rb +106 -91
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a08e75e08540db9629b91a3c82a460a901a857fa8d40db6180d8efec1fd2eb9e
|
|
4
|
+
data.tar.gz: cdf0f2d5a053346c412369b1751605b618c2e9fc6f8594dfd6a4c86b08e6a56f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 474a1577c07cda797f422caa596e35c2831d33c0acd8dfaf1dd5d1397f9c036118264bb814e0fb32a96c7e43db32f797fce1d0c8f2a36eeb187417ca45d9ccaa
|
|
7
|
+
data.tar.gz: 76298cd1b9e8a2648f48e695eb4d1199d12a24b5dc384b8d6f7acc9495769a8ef605403c0f9d4b0ec37192408850770ff72e7827be56f99124bc3f3855798d6f
|
data/README.md
CHANGED
|
@@ -244,7 +244,18 @@ In-depth docs live under [`docs/`](docs/README.md), organized via the
|
|
|
244
244
|
- **Tutorial:** [Getting started](docs/tutorials/getting-started.md)
|
|
245
245
|
- **How-to:** [Migrate from stdlib `::Regexp`](docs/how-to/migrate-from-stdlib-regexp.md), [Handle unsupported syntax](docs/how-to/handle-unsupported-syntax.md)
|
|
246
246
|
- **Reference:** [`Fast::Regexp`](docs/reference/fast-regexp.md), [`MatchData`](docs/reference/fast-regexp-matchdata.md), [`Set`](docs/reference/fast-regexp-set.md)
|
|
247
|
-
- **Explainers:** [Engine fallback](docs/explainers/engine-fallback.md)
|
|
247
|
+
- **Explainers:** [Engine fallback](docs/explainers/engine-fallback.md), [Benchmarks](docs/explainers/benchmarks.md)
|
|
248
|
+
|
|
249
|
+
## Performance
|
|
250
|
+
|
|
251
|
+
Matching runs 8–10x stdlib `::Regexp` on a hit or a miss, scanning 2–5x,
|
|
252
|
+
`gsub` with a template 7–13x; reading a `MatchData` costs the same objects
|
|
253
|
+
as `::MatchData`, and `match?`, `=~`, `===` and `Set#match?` allocate
|
|
254
|
+
nothing. Compiling a pattern is the one place rust/regex is slower (it
|
|
255
|
+
builds its automaton up front), so compile once and keep the object. Every
|
|
256
|
+
operation, with the stdlib equivalent beside it, is on the
|
|
257
|
+
[benchmarks page](docs/explainers/benchmarks.md); reproduce with
|
|
258
|
+
`bundle exec ruby benchmark/operations.rb`.
|
|
248
259
|
|
|
249
260
|
## Development
|
|
250
261
|
|
|
@@ -253,6 +264,7 @@ bin/setup # install deps
|
|
|
253
264
|
bin/console # interactive prompt to play around
|
|
254
265
|
rake compile # (re)compile extension
|
|
255
266
|
rake spec # run tests
|
|
267
|
+
bundle exec ruby benchmark/operations.rb # the benchmarks page's table
|
|
256
268
|
```
|
|
257
269
|
|
|
258
270
|
## Contributing
|
data/ext/fast_regexp/src/lib.rs
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
use magnus::{
|
|
2
|
-
function, method,
|
|
2
|
+
function, gc, method,
|
|
3
3
|
scan_args::{get_kwargs, scan_args},
|
|
4
|
-
value::ReprValue,
|
|
5
|
-
Error, Module, Object, RArray, RClass, RHash, RString, Ruby,
|
|
4
|
+
value::{Opaque, ReprValue},
|
|
5
|
+
DataTypeFunctions, Error, Integer, Module, Object, RArray, RClass, RHash, RString, Ruby,
|
|
6
|
+
Symbol, TryConvert, TypedData, Value,
|
|
6
7
|
};
|
|
7
8
|
use regex::bytes::{NoExpand, Regex, RegexBuilder, RegexSet, RegexSetBuilder};
|
|
8
9
|
use std::collections::HashMap;
|
|
9
|
-
use std::sync::Arc;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
use std::sync::{Arc, OnceLock};
|
|
11
|
+
|
|
12
|
+
/// A haystack's bytes, read in place for the duration of one native call.
|
|
13
|
+
///
|
|
14
|
+
/// Sound because the call hands nothing back to Ruby while the borrow lives:
|
|
15
|
+
/// the GVL is held and no block is yielded, so no Ruby code can mutate the
|
|
16
|
+
/// string, and a GC triggered by the result objects the call builds neither
|
|
17
|
+
/// frees nor moves it — a method argument is rooted and pinned on the VM
|
|
18
|
+
/// stack for the whole call. Paths that *do* run Ruby code mid-call (the
|
|
19
|
+
/// block forms of `sub`/`gsub`) read a frozen snapshot instead; see
|
|
20
|
+
/// [`FastMatchData`].
|
|
21
|
+
fn bytes(haystack: &RString) -> &[u8] {
|
|
22
|
+
unsafe { haystack.as_slice() }
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
fn utf8_string(ruby: &Ruby, bytes: &[u8]) -> RString {
|
|
@@ -17,31 +27,31 @@ fn utf8_string(ruby: &Ruby, bytes: &[u8]) -> RString {
|
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
fn arg_error(message: impl Into<String>) -> Error {
|
|
20
|
-
Error::new(
|
|
21
|
-
Ruby::get().unwrap().exception_arg_error(),
|
|
22
|
-
message.into(),
|
|
23
|
-
)
|
|
30
|
+
Error::new(Ruby::get().unwrap().exception_arg_error(), message.into())
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
fn type_error(message: impl Into<String>) -> Error {
|
|
27
|
-
Error::new(
|
|
28
|
-
Ruby::get().unwrap().exception_type_error(),
|
|
29
|
-
message.into(),
|
|
30
|
-
)
|
|
34
|
+
Error::new(Ruby::get().unwrap().exception_type_error(), message.into())
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
type CaptureOffset = Option<(usize, usize)>;
|
|
34
38
|
|
|
35
|
-
/// Inner state shared between a compiled regex and
|
|
36
|
-
/// produces
|
|
37
|
-
/// shared cheaply across many matches (e.g. from `#scan_matches`).
|
|
39
|
+
/// Inner state shared between a compiled regex and every [`FastMatchData`]
|
|
40
|
+
/// it produces, behind an `Arc`.
|
|
38
41
|
struct RegexInner {
|
|
39
42
|
regex: Regex,
|
|
40
43
|
/// `name -> capture index`. Empty when the pattern has no named captures.
|
|
41
44
|
names: HashMap<String, usize>,
|
|
42
|
-
///
|
|
43
|
-
/// the indices produced by `regex.captures_iter()`.
|
|
45
|
+
/// Capture-group names in index order (`None` for unnamed groups).
|
|
44
46
|
name_index: Vec<Option<String>>,
|
|
47
|
+
/// `name_index` as one Ruby Array of interned frozen Strings (nil for
|
|
48
|
+
/// unnamed groups), so `names` and `named_captures` hand out the same
|
|
49
|
+
/// String objects on every call. Built on first use, from a method of a
|
|
50
|
+
/// live wrapper, so something marks it from the moment it exists; built
|
|
51
|
+
/// into the Array as each name is interned, because the interning table
|
|
52
|
+
/// holds its strings only weakly and a GC between two interns would
|
|
53
|
+
/// otherwise free the first. Marked by every wrapper holding this `Arc`.
|
|
54
|
+
ruby_names: OnceLock<Opaque<RArray>>,
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
impl RegexInner {
|
|
@@ -51,26 +61,71 @@ impl RegexInner {
|
|
|
51
61
|
.build()
|
|
52
62
|
.map_err(|e| arg_error(e.to_string()))?;
|
|
53
63
|
|
|
54
|
-
let name_index: Vec<Option<String>> =
|
|
55
|
-
regex.capture_names().map(|n| n.map(String::from)).collect();
|
|
56
64
|
let mut names = HashMap::new();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
let mut name_index = Vec::with_capacity(regex.captures_len());
|
|
66
|
+
for (idx, name) in regex.capture_names().enumerate() {
|
|
67
|
+
name_index.push(name.map(|n| {
|
|
68
|
+
names.insert(n.to_owned(), idx);
|
|
69
|
+
n.to_owned()
|
|
70
|
+
}));
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
Ok(Self {
|
|
64
74
|
regex,
|
|
65
75
|
names,
|
|
66
76
|
name_index,
|
|
77
|
+
ruby_names: OnceLock::new(),
|
|
67
78
|
})
|
|
68
79
|
}
|
|
80
|
+
|
|
81
|
+
fn mark(&self, marker: &gc::Marker) {
|
|
82
|
+
if let Some(names) = self.ruby_names.get() {
|
|
83
|
+
marker.mark(*names);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The interned names Array (see the field), built on first use.
|
|
88
|
+
fn ruby_names(&self, ruby: &Ruby) -> RArray {
|
|
89
|
+
ruby.get_inner(*self.ruby_names.get_or_init(|| {
|
|
90
|
+
let names = ruby.ary_new_capa(self.name_index.len());
|
|
91
|
+
for name in &self.name_index {
|
|
92
|
+
let pushed = match name {
|
|
93
|
+
Some(name) => names.push(ruby.str_new(name).to_interned_str()),
|
|
94
|
+
None => names.push(()),
|
|
95
|
+
};
|
|
96
|
+
pushed.expect("pushing onto a fresh, unfrozen Array");
|
|
97
|
+
}
|
|
98
|
+
Opaque::from(names)
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Capture-group names in declaration order, unnamed groups skipped.
|
|
103
|
+
fn names(&self, ruby: &Ruby) -> RArray {
|
|
104
|
+
let names = self.ruby_names(ruby);
|
|
105
|
+
// SAFETY: the Array is reachable from the wrapper this runs on behalf
|
|
106
|
+
// of and no Ruby code runs while the slice is read.
|
|
107
|
+
let values = unsafe { names.as_slice() };
|
|
108
|
+
ruby.ary_from_iter(values.iter().skip(1).filter(|v| !v.is_nil()).copied())
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Every group's `(start, end)` for one match, index 0 the whole match.
|
|
112
|
+
fn offsets(&self, caps: ®ex::bytes::Captures) -> Vec<CaptureOffset> {
|
|
113
|
+
(0..self.regex.captures_len())
|
|
114
|
+
.map(|i| caps.get(i).map(|m| (m.start(), m.end())))
|
|
115
|
+
.collect()
|
|
116
|
+
}
|
|
69
117
|
}
|
|
70
118
|
|
|
71
|
-
#[
|
|
119
|
+
#[derive(TypedData)]
|
|
120
|
+
#[magnus(class = "Fast::Regexp::Native", free_immediately, size, mark)]
|
|
72
121
|
pub struct FastRegexp(Arc<RegexInner>);
|
|
73
122
|
|
|
123
|
+
impl DataTypeFunctions for FastRegexp {
|
|
124
|
+
fn mark(&self, marker: &gc::Marker) {
|
|
125
|
+
self.0.mark(marker);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
impl FastRegexp {
|
|
75
130
|
pub fn new(args: &[Value]) -> Result<Self, Error> {
|
|
76
131
|
let args = scan_args::<(String,), (), (), (), RHash, ()>(args)?;
|
|
@@ -83,61 +138,47 @@ impl FastRegexp {
|
|
|
83
138
|
Ok(Self(Arc::new(RegexInner::from_pattern(&pattern, unicode)?)))
|
|
84
139
|
}
|
|
85
140
|
|
|
86
|
-
fn
|
|
87
|
-
&self,
|
|
88
|
-
haystack: Arc<Vec<u8>>,
|
|
89
|
-
offsets: Vec<CaptureOffset>,
|
|
90
|
-
) -> FastMatchData {
|
|
141
|
+
fn match_data(&self, haystack: RString, captures: Vec<CaptureOffset>) -> FastMatchData {
|
|
91
142
|
FastMatchData {
|
|
92
|
-
haystack,
|
|
93
|
-
captures
|
|
143
|
+
haystack: haystack.into(),
|
|
144
|
+
captures,
|
|
94
145
|
inner: self.0.clone(),
|
|
95
146
|
}
|
|
96
147
|
}
|
|
97
148
|
|
|
149
|
+
/// First match as a MatchData, or nil. The search runs over the live
|
|
150
|
+
/// haystack; only a hit pays for the frozen snapshot the MatchData keeps.
|
|
98
151
|
pub fn rmatch(&self, haystack: RString) -> Option<FastMatchData> {
|
|
99
|
-
let
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
})?;
|
|
152
|
+
let inner = &self.0;
|
|
153
|
+
let captures = inner
|
|
154
|
+
.regex
|
|
155
|
+
.captures(bytes(&haystack))
|
|
156
|
+
.map(|caps| inner.offsets(&caps))?;
|
|
157
|
+
Some(self.match_data(RString::new_frozen(haystack), captures))
|
|
158
|
+
}
|
|
107
159
|
|
|
108
|
-
|
|
160
|
+
/// Byte offset of the first match, or nil — `=~` without a MatchData.
|
|
161
|
+
pub fn find(&self, haystack: RString) -> Option<usize> {
|
|
162
|
+
self.0.regex.find(bytes(&haystack)).map(|m| m.start())
|
|
109
163
|
}
|
|
110
164
|
|
|
111
165
|
pub fn scan(ruby: &Ruby, rb_self: &Self, haystack: RString) -> Result<RArray, Error> {
|
|
112
166
|
let regex = &rb_self.0.regex;
|
|
113
|
-
let bytes =
|
|
167
|
+
let bytes = bytes(&haystack);
|
|
114
168
|
|
|
115
169
|
if regex.captures_len() == 1 {
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
.
|
|
119
|
-
.collect();
|
|
120
|
-
let result = ruby.ary_new_capa(ranges.len());
|
|
121
|
-
for (s, e) in ranges {
|
|
122
|
-
result.push(utf8_string(ruby, &bytes[s..e]))?;
|
|
170
|
+
let result = ruby.ary_new();
|
|
171
|
+
for m in regex.find_iter(bytes) {
|
|
172
|
+
result.push(utf8_string(ruby, m.as_bytes()))?;
|
|
123
173
|
}
|
|
124
174
|
Ok(result)
|
|
125
175
|
} else {
|
|
126
|
-
let
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.collect()
|
|
133
|
-
})
|
|
134
|
-
.collect();
|
|
135
|
-
let result = ruby.ary_new_capa(groups.len());
|
|
136
|
-
for group_ranges in groups {
|
|
137
|
-
let group = ruby.ary_new_capa(group_ranges.len());
|
|
138
|
-
for range in group_ranges {
|
|
139
|
-
match range {
|
|
140
|
-
Some((s, e)) => group.push(utf8_string(ruby, &bytes[s..e]))?,
|
|
176
|
+
let result = ruby.ary_new();
|
|
177
|
+
for caps in regex.captures_iter(bytes) {
|
|
178
|
+
let group = ruby.ary_new_capa(regex.captures_len() - 1);
|
|
179
|
+
for m in caps.iter().skip(1) {
|
|
180
|
+
match m {
|
|
181
|
+
Some(m) => group.push(utf8_string(ruby, m.as_bytes()))?,
|
|
141
182
|
None => group.push(())?,
|
|
142
183
|
}
|
|
143
184
|
}
|
|
@@ -147,32 +188,29 @@ impl FastRegexp {
|
|
|
147
188
|
}
|
|
148
189
|
}
|
|
149
190
|
|
|
191
|
+
/// Every match as a MatchData, all sharing one frozen snapshot of the
|
|
192
|
+
/// haystack (taken only if there is at least one match).
|
|
150
193
|
pub fn scan_matches(ruby: &Ruby, rb_self: &Self, haystack: RString) -> Result<RArray, Error> {
|
|
151
|
-
let
|
|
152
|
-
let
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.captures_iter(&bytes)
|
|
157
|
-
.map(|caps| {
|
|
158
|
-
(0..n_groups)
|
|
159
|
-
.map(|i| caps.get(i).map(|m| (m.start(), m.end())))
|
|
160
|
-
.collect()
|
|
161
|
-
})
|
|
194
|
+
let inner = &rb_self.0;
|
|
195
|
+
let all: Vec<Vec<CaptureOffset>> = inner
|
|
196
|
+
.regex
|
|
197
|
+
.captures_iter(bytes(&haystack))
|
|
198
|
+
.map(|caps| inner.offsets(&caps))
|
|
162
199
|
.collect();
|
|
163
200
|
|
|
164
|
-
let shared = Arc::new(bytes);
|
|
165
201
|
let result = ruby.ary_new_capa(all.len());
|
|
166
|
-
|
|
167
|
-
result
|
|
202
|
+
if all.is_empty() {
|
|
203
|
+
return Ok(result);
|
|
204
|
+
}
|
|
205
|
+
let snapshot = RString::new_frozen(haystack);
|
|
206
|
+
for captures in all {
|
|
207
|
+
result.push(rb_self.match_data(snapshot, captures))?;
|
|
168
208
|
}
|
|
169
209
|
Ok(result)
|
|
170
210
|
}
|
|
171
211
|
|
|
172
212
|
pub fn is_match(&self, haystack: RString) -> bool {
|
|
173
|
-
|
|
174
|
-
let bytes = haystack_bytes(&haystack);
|
|
175
|
-
regex.is_match(&bytes)
|
|
213
|
+
self.0.regex.is_match(bytes(&haystack))
|
|
176
214
|
}
|
|
177
215
|
|
|
178
216
|
pub fn sub_str(
|
|
@@ -183,12 +221,11 @@ impl FastRegexp {
|
|
|
183
221
|
literal: bool,
|
|
184
222
|
) -> RString {
|
|
185
223
|
let regex = &rb_self.0.regex;
|
|
186
|
-
let bytes =
|
|
187
|
-
let
|
|
188
|
-
|
|
189
|
-
regex.replace(&bytes, NoExpand(&repl)).into_owned()
|
|
224
|
+
let (bytes, repl) = (bytes(&haystack), bytes(&replacement));
|
|
225
|
+
let out = if literal {
|
|
226
|
+
regex.replace(bytes, NoExpand(repl))
|
|
190
227
|
} else {
|
|
191
|
-
regex.replace(
|
|
228
|
+
regex.replace(bytes, repl)
|
|
192
229
|
};
|
|
193
230
|
utf8_string(ruby, &out)
|
|
194
231
|
}
|
|
@@ -201,16 +238,70 @@ impl FastRegexp {
|
|
|
201
238
|
literal: bool,
|
|
202
239
|
) -> RString {
|
|
203
240
|
let regex = &rb_self.0.regex;
|
|
204
|
-
let bytes =
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
regex.replace_all(&bytes, NoExpand(&repl)).into_owned()
|
|
241
|
+
let (bytes, repl) = (bytes(&haystack), bytes(&replacement));
|
|
242
|
+
let out = if literal {
|
|
243
|
+
regex.replace_all(bytes, NoExpand(repl))
|
|
208
244
|
} else {
|
|
209
|
-
regex.replace_all(
|
|
245
|
+
regex.replace_all(bytes, repl)
|
|
210
246
|
};
|
|
211
247
|
utf8_string(ruby, &out)
|
|
212
248
|
}
|
|
213
249
|
|
|
250
|
+
/// The block forms of `sub` (`limit` 1) and `gsub` (no limit) in one
|
|
251
|
+
/// pass: each match is yielded as a MatchData and the block's result
|
|
252
|
+
/// (via `to_s`) is spliced in for it. The block runs arbitrary Ruby, so
|
|
253
|
+
/// everything is read from a frozen snapshot of the haystack rather than
|
|
254
|
+
/// the live string — the same snapshot every yielded MatchData keeps.
|
|
255
|
+
fn replace_block(
|
|
256
|
+
ruby: &Ruby,
|
|
257
|
+
rb_self: &Self,
|
|
258
|
+
haystack: RString,
|
|
259
|
+
limit: Option<usize>,
|
|
260
|
+
) -> Result<RString, Error> {
|
|
261
|
+
let inner = &rb_self.0;
|
|
262
|
+
if !inner.regex.is_match(bytes(&haystack)) {
|
|
263
|
+
// No match: a copy of the haystack, encoding and all, as
|
|
264
|
+
// `String#sub`/`#gsub` return (copy-on-write, nothing copied yet).
|
|
265
|
+
return Ok(RString::new_shared(haystack));
|
|
266
|
+
}
|
|
267
|
+
let snapshot = RString::new_frozen(haystack);
|
|
268
|
+
let bytes = bytes(&snapshot);
|
|
269
|
+
// Built as a UTF-8 String from the start: the haystack's own bytes go
|
|
270
|
+
// in raw (`cat`), each block result through Ruby's encoding
|
|
271
|
+
// negotiation (`buf_append`, as `String#gsub` appends), so an
|
|
272
|
+
// incompatible replacement raises Encoding::CompatibilityError rather
|
|
273
|
+
// than leaving invalid bytes behind. The block's result is taken as
|
|
274
|
+
// `rb_obj_as_string` takes it: a String as is, anything else via
|
|
275
|
+
// `to_s`, falling back to `Object#to_s` when that returns a non-String.
|
|
276
|
+
let out = utf8_string(ruby, &[]);
|
|
277
|
+
let mut cursor = 0;
|
|
278
|
+
for caps in inner
|
|
279
|
+
.regex
|
|
280
|
+
.captures_iter(bytes)
|
|
281
|
+
.take(limit.unwrap_or(usize::MAX))
|
|
282
|
+
{
|
|
283
|
+
let whole = caps.get(0).expect("group 0 is the match");
|
|
284
|
+
out.cat(&bytes[cursor..whole.start()]);
|
|
285
|
+
let replacement: Value =
|
|
286
|
+
ruby.yield_value(rb_self.match_data(snapshot, inner.offsets(&caps)))?;
|
|
287
|
+
out.buf_append(replacement.to_r_string()?)?;
|
|
288
|
+
cursor = whole.end();
|
|
289
|
+
}
|
|
290
|
+
out.cat(&bytes[cursor..]);
|
|
291
|
+
// Keep the snapshot reachable from this frame until the last read of
|
|
292
|
+
// `bytes` above, whatever the yielded MatchData objects' fate.
|
|
293
|
+
std::hint::black_box(snapshot);
|
|
294
|
+
Ok(out)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
pub fn sub_block(ruby: &Ruby, rb_self: &Self, haystack: RString) -> Result<RString, Error> {
|
|
298
|
+
Self::replace_block(ruby, rb_self, haystack, Some(1))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
pub fn gsub_block(ruby: &Ruby, rb_self: &Self, haystack: RString) -> Result<RString, Error> {
|
|
302
|
+
Self::replace_block(ruby, rb_self, haystack, None)
|
|
303
|
+
}
|
|
304
|
+
|
|
214
305
|
pub fn pattern(&self) -> &str {
|
|
215
306
|
self.0.regex.as_str()
|
|
216
307
|
}
|
|
@@ -220,27 +311,46 @@ impl FastRegexp {
|
|
|
220
311
|
self.0.regex.captures_len() - 1
|
|
221
312
|
}
|
|
222
313
|
|
|
223
|
-
pub fn names(ruby: &Ruby, rb_self: &Self) ->
|
|
224
|
-
|
|
225
|
-
for name in rb_self.0.name_index.iter().skip(1).flatten() {
|
|
226
|
-
arr.push(name.as_str())?;
|
|
227
|
-
}
|
|
228
|
-
Ok(arr)
|
|
314
|
+
pub fn names(ruby: &Ruby, rb_self: &Self) -> RArray {
|
|
315
|
+
rb_self.0.names(ruby)
|
|
229
316
|
}
|
|
230
317
|
}
|
|
231
318
|
|
|
232
|
-
|
|
319
|
+
/// One match: capture offsets into a frozen snapshot of the haystack. The
|
|
320
|
+
/// snapshot is `rb_str_new_frozen` of the caller's string — the string
|
|
321
|
+
/// itself when it was already frozen, otherwise a frozen copy-on-write
|
|
322
|
+
/// sibling sharing its buffer — so a later mutation of the caller's string
|
|
323
|
+
/// can't reach it, and nothing was copied to guarantee that.
|
|
324
|
+
#[derive(TypedData)]
|
|
325
|
+
#[magnus(
|
|
326
|
+
class = "Fast::Regexp::Native::MatchData",
|
|
327
|
+
free_immediately,
|
|
328
|
+
size,
|
|
329
|
+
mark
|
|
330
|
+
)]
|
|
233
331
|
pub struct FastMatchData {
|
|
234
|
-
haystack:
|
|
332
|
+
haystack: Opaque<RString>,
|
|
235
333
|
/// Index 0 is the whole match. Subsequent entries are capture groups in
|
|
236
334
|
/// order; `None` indicates a group that did not participate.
|
|
237
335
|
captures: Vec<CaptureOffset>,
|
|
238
336
|
inner: Arc<RegexInner>,
|
|
239
337
|
}
|
|
240
338
|
|
|
339
|
+
impl DataTypeFunctions for FastMatchData {
|
|
340
|
+
fn mark(&self, marker: &gc::Marker) {
|
|
341
|
+
marker.mark(self.haystack);
|
|
342
|
+
self.inner.mark(marker);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
241
346
|
impl FastMatchData {
|
|
347
|
+
fn haystack(&self, ruby: &Ruby) -> RString {
|
|
348
|
+
ruby.get_inner(self.haystack)
|
|
349
|
+
}
|
|
350
|
+
|
|
242
351
|
fn slice(&self, ruby: &Ruby, range: (usize, usize)) -> RString {
|
|
243
|
-
|
|
352
|
+
let haystack = self.haystack(ruby);
|
|
353
|
+
utf8_string(ruby, &bytes(&haystack)[range.0..range.1])
|
|
244
354
|
}
|
|
245
355
|
|
|
246
356
|
fn capture_index(&self, idx: i64) -> Option<usize> {
|
|
@@ -255,21 +365,40 @@ impl FastMatchData {
|
|
|
255
365
|
|
|
256
366
|
/// Returns `Some(Some(range))` for a participating group, `Some(None)` for
|
|
257
367
|
/// a known-but-non-participating group, `None` for an out-of-range index
|
|
258
|
-
/// or an unknown capture name.
|
|
368
|
+
/// or an unknown capture name. Dispatches on the key's type rather than
|
|
369
|
+
/// attempting conversions: a failed conversion raises (and allocates an
|
|
370
|
+
/// exception) on the way to being caught.
|
|
259
371
|
fn resolve(&self, key: Value) -> Result<Option<CaptureOffset>, Error> {
|
|
260
|
-
if let
|
|
261
|
-
return Ok(self
|
|
372
|
+
if let Some(index) = Integer::from_value(key) {
|
|
373
|
+
return Ok(self
|
|
374
|
+
.capture_index(index.to_i64()?)
|
|
375
|
+
.map(|i| self.captures[i]));
|
|
376
|
+
}
|
|
377
|
+
let by_name = |name: &str| self.inner.names.get(name).map(|&i| self.captures[i]);
|
|
378
|
+
if let Some(symbol) = Symbol::from_value(key) {
|
|
379
|
+
return Ok(by_name(&symbol.name()?));
|
|
262
380
|
}
|
|
263
|
-
if let
|
|
264
|
-
|
|
265
|
-
|
|
381
|
+
if let Some(name) = RString::from_value(key) {
|
|
382
|
+
// A UTF-8 (or ASCII) name is read in place; any other encoding
|
|
383
|
+
// is transcoded, and one that can't be names no group.
|
|
384
|
+
return Ok(match unsafe { name.as_str() } {
|
|
385
|
+
Ok(name) => by_name(name),
|
|
386
|
+
Err(_) => name.to_string().ok().and_then(|name| by_name(&name)),
|
|
387
|
+
});
|
|
266
388
|
}
|
|
267
|
-
|
|
268
|
-
|
|
389
|
+
// Anything else that converts to an Integer (a Float, a `to_int`
|
|
390
|
+
// object) indexes like one, as `::MatchData#[]` allows; anything
|
|
391
|
+
// that converts to a String (`to_str`) names a group, as this class
|
|
392
|
+
// has always allowed.
|
|
393
|
+
if let Ok(index) = i64::try_convert(key) {
|
|
394
|
+
return Ok(self.capture_index(index).map(|i| self.captures[i]));
|
|
395
|
+
}
|
|
396
|
+
match RString::try_convert(key) {
|
|
397
|
+
Ok(name) => Ok(by_name(&name.to_string()?)),
|
|
398
|
+
Err(_) => Err(type_error(
|
|
399
|
+
"no implicit conversion of capture key into Integer, String, or Symbol",
|
|
400
|
+
)),
|
|
269
401
|
}
|
|
270
|
-
Err(type_error(
|
|
271
|
-
"no implicit conversion of capture key into Integer, String, or Symbol",
|
|
272
|
-
))
|
|
273
402
|
}
|
|
274
403
|
|
|
275
404
|
fn whole(&self) -> (usize, usize) {
|
|
@@ -283,49 +412,47 @@ impl FastMatchData {
|
|
|
283
412
|
}
|
|
284
413
|
}
|
|
285
414
|
|
|
286
|
-
|
|
287
|
-
let arr = ruby.ary_new_capa(
|
|
288
|
-
for cap in
|
|
415
|
+
fn slices(&self, ruby: &Ruby, captures: &[CaptureOffset]) -> Result<RArray, Error> {
|
|
416
|
+
let arr = ruby.ary_new_capa(captures.len());
|
|
417
|
+
for cap in captures {
|
|
289
418
|
match cap {
|
|
290
|
-
Some(r) => arr.push(
|
|
419
|
+
Some(r) => arr.push(self.slice(ruby, *r))?,
|
|
291
420
|
None => arr.push(())?,
|
|
292
421
|
}
|
|
293
422
|
}
|
|
294
423
|
Ok(arr)
|
|
295
424
|
}
|
|
296
425
|
|
|
426
|
+
pub fn to_a(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
427
|
+
rb_self.slices(ruby, &rb_self.captures)
|
|
428
|
+
}
|
|
429
|
+
|
|
297
430
|
pub fn captures(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
298
|
-
|
|
299
|
-
for cap in rb_self.captures.iter().skip(1) {
|
|
300
|
-
match cap {
|
|
301
|
-
Some(r) => arr.push(rb_self.slice(ruby, *r))?,
|
|
302
|
-
None => arr.push(())?,
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
Ok(arr)
|
|
431
|
+
rb_self.slices(ruby, &rb_self.captures[1..])
|
|
306
432
|
}
|
|
307
433
|
|
|
308
434
|
pub fn named_captures(ruby: &Ruby, rb_self: &Self) -> Result<RHash, Error> {
|
|
309
435
|
let hash = ruby.hash_new();
|
|
436
|
+
let names = rb_self.inner.ruby_names(ruby);
|
|
437
|
+
// SAFETY: the Array is reachable from this match's wrapper and no
|
|
438
|
+
// Ruby code runs while the slice is read.
|
|
439
|
+
let names = unsafe { names.as_slice() };
|
|
310
440
|
// Iterate in declaration order so the hash preserves regex order.
|
|
311
|
-
for (idx, name) in
|
|
312
|
-
if
|
|
313
|
-
|
|
314
|
-
Some(r) => rb_self.slice(ruby, r).as_value(),
|
|
315
|
-
None => ruby.qnil().as_value(),
|
|
316
|
-
};
|
|
317
|
-
hash.aset(name.as_str(), value)?;
|
|
441
|
+
for (idx, name) in names.iter().enumerate() {
|
|
442
|
+
if name.is_nil() {
|
|
443
|
+
continue;
|
|
318
444
|
}
|
|
445
|
+
let value: Value = match rb_self.captures[idx] {
|
|
446
|
+
Some(r) => rb_self.slice(ruby, r).as_value(),
|
|
447
|
+
None => ruby.qnil().as_value(),
|
|
448
|
+
};
|
|
449
|
+
hash.aset(*name, value)?;
|
|
319
450
|
}
|
|
320
451
|
Ok(hash)
|
|
321
452
|
}
|
|
322
453
|
|
|
323
|
-
pub fn names(ruby: &Ruby, rb_self: &Self) ->
|
|
324
|
-
|
|
325
|
-
for name in rb_self.inner.name_index.iter().skip(1).flatten() {
|
|
326
|
-
arr.push(name.as_str())?;
|
|
327
|
-
}
|
|
328
|
-
Ok(arr)
|
|
454
|
+
pub fn names(ruby: &Ruby, rb_self: &Self) -> RArray {
|
|
455
|
+
rb_self.inner.names(ruby)
|
|
329
456
|
}
|
|
330
457
|
|
|
331
458
|
pub fn size(&self) -> usize {
|
|
@@ -334,22 +461,23 @@ impl FastMatchData {
|
|
|
334
461
|
|
|
335
462
|
pub fn pre_match(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
336
463
|
let (s, _) = rb_self.whole();
|
|
337
|
-
|
|
464
|
+
rb_self.slice(ruby, (0, s))
|
|
338
465
|
}
|
|
339
466
|
|
|
340
467
|
pub fn post_match(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
341
468
|
let (_, e) = rb_self.whole();
|
|
342
|
-
|
|
469
|
+
let haystack = rb_self.haystack(ruby);
|
|
470
|
+
let len = bytes(&haystack).len();
|
|
471
|
+
rb_self.slice(ruby, (e, len))
|
|
343
472
|
}
|
|
344
473
|
|
|
345
474
|
pub fn whole_match(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
346
475
|
rb_self.slice(ruby, rb_self.whole())
|
|
347
476
|
}
|
|
348
477
|
|
|
478
|
+
/// The frozen snapshot the match was taken over (see the struct doc).
|
|
349
479
|
pub fn string(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
350
|
-
|
|
351
|
-
s.freeze();
|
|
352
|
-
s
|
|
480
|
+
rb_self.haystack(ruby)
|
|
353
481
|
}
|
|
354
482
|
|
|
355
483
|
pub fn byteoffset(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<Value, Error> {
|
|
@@ -384,7 +512,8 @@ impl FastMatchData {
|
|
|
384
512
|
|
|
385
513
|
pub fn inspect(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
386
514
|
let (s, e) = rb_self.whole();
|
|
387
|
-
let
|
|
515
|
+
let haystack = rb_self.haystack(ruby);
|
|
516
|
+
let matched = String::from_utf8_lossy(&bytes(&haystack)[s..e]);
|
|
388
517
|
ruby.str_new(&format!("#<Fast::Regexp::MatchData {:?}>", matched))
|
|
389
518
|
}
|
|
390
519
|
}
|
|
@@ -410,15 +539,11 @@ impl FastRegexpSet {
|
|
|
410
539
|
}
|
|
411
540
|
|
|
412
541
|
pub fn matches(&self, haystack: RString) -> Vec<usize> {
|
|
413
|
-
|
|
414
|
-
let bytes = haystack_bytes(&haystack);
|
|
415
|
-
set.matches(&bytes).iter().collect()
|
|
542
|
+
self.0.matches(bytes(&haystack)).iter().collect()
|
|
416
543
|
}
|
|
417
544
|
|
|
418
545
|
pub fn is_match(&self, haystack: RString) -> bool {
|
|
419
|
-
|
|
420
|
-
let bytes = haystack_bytes(&haystack);
|
|
421
|
-
set.is_match(&bytes)
|
|
546
|
+
self.0.is_match(bytes(&haystack))
|
|
422
547
|
}
|
|
423
548
|
|
|
424
549
|
pub fn patterns(&self) -> Vec<String> {
|
|
@@ -429,15 +554,19 @@ impl FastRegexpSet {
|
|
|
429
554
|
#[magnus::init]
|
|
430
555
|
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
431
556
|
let object_class = ruby.class_object();
|
|
432
|
-
// Fast::Regexp must already be defined
|
|
433
|
-
// before this extension is required — the Ruby façade
|
|
434
|
-
//
|
|
557
|
+
// Fast::Regexp and Fast::Regexp::MatchData must already be defined by
|
|
558
|
+
// lib/fast_regexp.rb before this extension is required — the Ruby façade
|
|
559
|
+
// owns those constants; the Native classes registered below sit under
|
|
560
|
+
// them, and Native::MatchData subclasses the façade's MatchData so a
|
|
561
|
+
// native match is a Fast::Regexp::MatchData with no wrapper around it.
|
|
435
562
|
let fast_module: magnus::RModule = object_class.const_get("Fast")?;
|
|
436
563
|
let regexp_facade: RClass = fast_module.const_get("Regexp")?;
|
|
564
|
+
let match_data_facade: RClass = regexp_facade.const_get("MatchData")?;
|
|
437
565
|
let regexp_class = regexp_facade.define_class("Native", object_class)?;
|
|
438
566
|
|
|
439
567
|
regexp_class.define_singleton_method("_native_new", function!(FastRegexp::new, -1))?;
|
|
440
568
|
regexp_class.define_method("_native_match", method!(FastRegexp::rmatch, 1))?;
|
|
569
|
+
regexp_class.define_method("_native_find", method!(FastRegexp::find, 1))?;
|
|
441
570
|
regexp_class.define_method("match?", method!(FastRegexp::is_match, 1))?;
|
|
442
571
|
regexp_class.define_method("scan", method!(FastRegexp::scan, 1))?;
|
|
443
572
|
regexp_class.define_method("scan_matches", method!(FastRegexp::scan_matches, 1))?;
|
|
@@ -446,8 +575,10 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
446
575
|
regexp_class.define_method("names", method!(FastRegexp::names, 0))?;
|
|
447
576
|
regexp_class.define_method("_native_sub", method!(FastRegexp::sub_str, 3))?;
|
|
448
577
|
regexp_class.define_method("_native_gsub", method!(FastRegexp::gsub_str, 3))?;
|
|
578
|
+
regexp_class.define_method("_native_sub_block", method!(FastRegexp::sub_block, 1))?;
|
|
579
|
+
regexp_class.define_method("_native_gsub_block", method!(FastRegexp::gsub_block, 1))?;
|
|
449
580
|
|
|
450
|
-
let match_data_class = regexp_class.define_class("MatchData",
|
|
581
|
+
let match_data_class = regexp_class.define_class("MatchData", match_data_facade)?;
|
|
451
582
|
match_data_class.define_method("[]", method!(FastMatchData::aref, 1))?;
|
|
452
583
|
match_data_class.define_method("to_a", method!(FastMatchData::to_a, 0))?;
|
|
453
584
|
match_data_class.define_method("captures", method!(FastMatchData::captures, 0))?;
|
data/lib/fast_regexp/version.rb
CHANGED
data/lib/fast_regexp.rb
CHANGED
|
@@ -21,6 +21,62 @@ module Fast
|
|
|
21
21
|
candidates = [File.join(base, abi, "fast_regexp"), File.join(base, "fast_regexp")]
|
|
22
22
|
candidates.find { |stem| NATIVE_EXTENSIONS.any? { |ext| File.exist?(stem + ext) } }
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# One match, whichever engine produced it. On the fast path the match
|
|
26
|
+
# *is* a `Fast::Regexp::Native::MatchData`, a subclass of this one
|
|
27
|
+
# defined by the extension (so it must exist before the extension
|
|
28
|
+
# loads); on the stdlib path it's an instance of this class wrapping the
|
|
29
|
+
# `::MatchData`. Same public surface either way.
|
|
30
|
+
class MatchData
|
|
31
|
+
include Enumerable
|
|
32
|
+
|
|
33
|
+
attr_reader :backend
|
|
34
|
+
|
|
35
|
+
def initialize(backend)
|
|
36
|
+
@backend = backend
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def native? = false
|
|
40
|
+
def stdlib? = true
|
|
41
|
+
|
|
42
|
+
def native = nil
|
|
43
|
+
def stdlib = @backend
|
|
44
|
+
|
|
45
|
+
def [](key) = @backend[key]
|
|
46
|
+
def to_a = @backend.to_a
|
|
47
|
+
def captures = @backend.captures
|
|
48
|
+
def named_captures = @backend.named_captures
|
|
49
|
+
def names = @backend.names
|
|
50
|
+
def size = @backend.size
|
|
51
|
+
alias_method :length, :size
|
|
52
|
+
def pre_match = @backend.pre_match
|
|
53
|
+
def post_match = @backend.post_match
|
|
54
|
+
def to_s = @backend.to_s
|
|
55
|
+
alias_method :match, :to_s
|
|
56
|
+
|
|
57
|
+
# The haystack the match was taken over: `::MatchData#string` is already
|
|
58
|
+
# a frozen snapshot, the same thing the native subclass keeps.
|
|
59
|
+
def string = @backend.string
|
|
60
|
+
|
|
61
|
+
# Byte-based offsets. `::MatchData#byteoffset` exists since Ruby 3.2;
|
|
62
|
+
# its `byte_begin`/`byte_end` don't (3.4 added `bytebegin`/`byteend`),
|
|
63
|
+
# so those two derive from `byteoffset` here.
|
|
64
|
+
def byteoffset(key) = @backend.byteoffset(key)
|
|
65
|
+
def byte_begin(key) = byteoffset(key)[0]
|
|
66
|
+
def byte_end(key) = byteoffset(key)[1]
|
|
67
|
+
|
|
68
|
+
def each(&block) = to_a.each(&block)
|
|
69
|
+
def values_at(*indices) = indices.map { |i| self[i] }
|
|
70
|
+
|
|
71
|
+
def ==(other)
|
|
72
|
+
other.is_a?(MatchData) && to_a == other.to_a && string == other.string
|
|
73
|
+
end
|
|
74
|
+
alias_method :eql?, :==
|
|
75
|
+
|
|
76
|
+
def hash = [to_a, string].hash
|
|
77
|
+
|
|
78
|
+
def inspect = "#<Fast::Regexp::MatchData #{to_s.inspect}>"
|
|
79
|
+
end
|
|
24
80
|
end
|
|
25
81
|
end
|
|
26
82
|
|
|
@@ -30,6 +86,22 @@ require native
|
|
|
30
86
|
|
|
31
87
|
module Fast
|
|
32
88
|
class Regexp
|
|
89
|
+
class Native
|
|
90
|
+
class MatchData
|
|
91
|
+
def native? = true
|
|
92
|
+
def stdlib? = false
|
|
93
|
+
|
|
94
|
+
def native = self
|
|
95
|
+
def stdlib = nil
|
|
96
|
+
def backend = self
|
|
97
|
+
|
|
98
|
+
# Immutable, so a copy is the object itself (the wrapped class has no
|
|
99
|
+
# allocator for `dup`/`clone` to go through).
|
|
100
|
+
def dup = self
|
|
101
|
+
def clone(freeze: nil) = self
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
33
105
|
RUBY_FLAG_MAP = {
|
|
34
106
|
::Regexp::IGNORECASE => "i",
|
|
35
107
|
::Regexp::EXTENDED => "x",
|
|
@@ -45,13 +117,21 @@ module Fast
|
|
|
45
117
|
allocate.tap { |re| re.send(:initialize, translated, original: pattern, **opts) }
|
|
46
118
|
end
|
|
47
119
|
|
|
48
|
-
# Bulk-compile a
|
|
49
|
-
#
|
|
120
|
+
# Bulk-compile a set of patterns. Two call shapes:
|
|
121
|
+
#
|
|
122
|
+
# Fast::Regexp.create_many(word: '\w+', num: '\d+')
|
|
123
|
+
# # => { word: #<Fast::Regexp ...>, num: #<Fast::Regexp ...> }
|
|
50
124
|
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
125
|
+
# Fast::Regexp.create_many('\w+', '\d+')
|
|
126
|
+
# # => [#<Fast::Regexp ...>, #<Fast::Regexp ...>]
|
|
127
|
+
#
|
|
128
|
+
# Mixing the two raises ArgumentError — pick one shape per call.
|
|
129
|
+
def create_many(*patterns, **named)
|
|
130
|
+
if !patterns.empty? && !named.empty?
|
|
131
|
+
raise ArgumentError, "create_many accepts positional patterns OR keyword patterns, not both"
|
|
132
|
+
end
|
|
133
|
+
return named.transform_values { |pat| new(pat) } if patterns.empty?
|
|
134
|
+
patterns.map { |pat| new(pat) }
|
|
55
135
|
end
|
|
56
136
|
|
|
57
137
|
private
|
|
@@ -88,8 +168,9 @@ module Fast
|
|
|
88
168
|
|
|
89
169
|
def match(haystack)
|
|
90
170
|
haystack = coerce_string(haystack)
|
|
91
|
-
|
|
92
|
-
|
|
171
|
+
return @backend._native_match(haystack) if fast?
|
|
172
|
+
m = @backend.match(haystack)
|
|
173
|
+
m && MatchData.new(m)
|
|
93
174
|
end
|
|
94
175
|
|
|
95
176
|
def match?(haystack)
|
|
@@ -105,8 +186,10 @@ module Fast
|
|
|
105
186
|
# also returns bytes here for API consistency).
|
|
106
187
|
def =~(other)
|
|
107
188
|
return nil unless other.respond_to?(:to_str)
|
|
108
|
-
|
|
109
|
-
|
|
189
|
+
haystack = other.to_str
|
|
190
|
+
return @backend._native_find(haystack) if fast?
|
|
191
|
+
m = @backend.match(haystack)
|
|
192
|
+
m && m.byteoffset(0)[0]
|
|
110
193
|
end
|
|
111
194
|
|
|
112
195
|
def scan(haystack)
|
|
@@ -115,22 +198,17 @@ module Fast
|
|
|
115
198
|
|
|
116
199
|
def scan_matches(haystack)
|
|
117
200
|
haystack = coerce_string(haystack)
|
|
118
|
-
if fast?
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match, haystack) }
|
|
123
|
-
results
|
|
124
|
-
end
|
|
201
|
+
return @backend.scan_matches(haystack) if fast?
|
|
202
|
+
results = []
|
|
203
|
+
haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match) }
|
|
204
|
+
results
|
|
125
205
|
end
|
|
126
206
|
|
|
127
207
|
def sub(haystack, replacement = nil, literal: false, &block)
|
|
128
208
|
haystack = coerce_string(haystack)
|
|
129
|
-
if
|
|
209
|
+
if block_given?
|
|
130
210
|
raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
|
|
131
|
-
|
|
132
|
-
return haystack.dup unless m
|
|
133
|
-
"#{m.pre_match}#{block.call(m)}#{m.post_match}"
|
|
211
|
+
fast? ? @backend._native_sub_block(haystack, &block) : stdlib_sub_with_block(haystack, &block)
|
|
134
212
|
else
|
|
135
213
|
raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
|
|
136
214
|
replacement = coerce_string(replacement)
|
|
@@ -144,9 +222,9 @@ module Fast
|
|
|
144
222
|
|
|
145
223
|
def gsub(haystack, replacement = nil, literal: false, &block)
|
|
146
224
|
haystack = coerce_string(haystack)
|
|
147
|
-
if
|
|
225
|
+
if block_given?
|
|
148
226
|
raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
|
|
149
|
-
fast? ?
|
|
227
|
+
fast? ? @backend._native_gsub_block(haystack, &block) : stdlib_gsub_with_block(haystack, &block)
|
|
150
228
|
else
|
|
151
229
|
raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
|
|
152
230
|
replacement = coerce_string(replacement)
|
|
@@ -246,29 +324,14 @@ module Fast
|
|
|
246
324
|
end
|
|
247
325
|
end
|
|
248
326
|
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
matches = scan_matches(haystack)
|
|
254
|
-
return haystack.dup if matches.empty?
|
|
255
|
-
|
|
256
|
-
out = String.new(encoding: Encoding::UTF_8)
|
|
257
|
-
cursor = 0
|
|
258
|
-
matches.each do |m|
|
|
259
|
-
bs, be = m.byteoffset(0)
|
|
260
|
-
out << haystack.byteslice(cursor, bs - cursor) if bs > cursor
|
|
261
|
-
out << yield(m).to_s
|
|
262
|
-
cursor = be
|
|
263
|
-
end
|
|
264
|
-
out << haystack.byteslice(cursor, haystack.bytesize - cursor) if cursor < haystack.bytesize
|
|
265
|
-
out
|
|
327
|
+
# Stdlib path: String#sub/#gsub already do single-pass iterate-and-replace
|
|
328
|
+
# and set $~ inside the block, so wrap the current ::MatchData and yield.
|
|
329
|
+
def stdlib_sub_with_block(haystack)
|
|
330
|
+
haystack.sub(@backend) { yield(MatchData.new(::Regexp.last_match)).to_s }
|
|
266
331
|
end
|
|
267
332
|
|
|
268
|
-
# Stdlib path: String#gsub already does single-pass iterate-and-replace
|
|
269
|
-
# and sets $~ inside the block, so wrap the current ::MatchData and yield.
|
|
270
333
|
def stdlib_gsub_with_block(haystack)
|
|
271
|
-
haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match
|
|
334
|
+
haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match)).to_s }
|
|
272
335
|
end
|
|
273
336
|
|
|
274
337
|
def coerce_string(value)
|
|
@@ -276,53 +339,5 @@ module Fast
|
|
|
276
339
|
return value.to_str if value.respond_to?(:to_str)
|
|
277
340
|
raise TypeError, "no implicit conversion of #{value.class} into String"
|
|
278
341
|
end
|
|
279
|
-
|
|
280
|
-
# Wraps either a Fast::Regexp::Native::MatchData or a stdlib ::MatchData
|
|
281
|
-
# so callers see one type regardless of which backend ran.
|
|
282
|
-
class MatchData
|
|
283
|
-
include Enumerable
|
|
284
|
-
|
|
285
|
-
attr_reader :backend, :string
|
|
286
|
-
|
|
287
|
-
def initialize(backend, haystack)
|
|
288
|
-
@backend = backend
|
|
289
|
-
@string = haystack
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def native? = @backend.is_a?(Fast::Regexp::Native::MatchData)
|
|
293
|
-
def stdlib? = !native?
|
|
294
|
-
|
|
295
|
-
def native = native? ? @backend : nil
|
|
296
|
-
def stdlib = stdlib? ? @backend : nil
|
|
297
|
-
|
|
298
|
-
def [](key) = @backend[key]
|
|
299
|
-
def to_a = @backend.to_a
|
|
300
|
-
def captures = @backend.captures
|
|
301
|
-
def named_captures = @backend.named_captures
|
|
302
|
-
def names = @backend.names
|
|
303
|
-
def size = @backend.size
|
|
304
|
-
alias_method :length, :size
|
|
305
|
-
def pre_match = @backend.pre_match
|
|
306
|
-
def post_match = @backend.post_match
|
|
307
|
-
def to_s = @backend.to_s
|
|
308
|
-
|
|
309
|
-
# Byte-based offsets. Both backends expose these (stdlib MatchData has
|
|
310
|
-
# `byteoffset` / `byte_begin` / `byte_end` since Ruby 3.2).
|
|
311
|
-
def byteoffset(key) = @backend.byteoffset(key)
|
|
312
|
-
def byte_begin(key) = @backend.byte_begin(key)
|
|
313
|
-
def byte_end(key) = @backend.byte_end(key)
|
|
314
|
-
|
|
315
|
-
def each(&block) = to_a.each(&block)
|
|
316
|
-
def values_at(*indices) = indices.map { |i| self[i] }
|
|
317
|
-
|
|
318
|
-
def ==(other)
|
|
319
|
-
other.is_a?(MatchData) && to_a == other.to_a && string == other.string
|
|
320
|
-
end
|
|
321
|
-
alias_method :eql?, :==
|
|
322
|
-
|
|
323
|
-
def hash = [to_a, string].hash
|
|
324
|
-
|
|
325
|
-
def inspect = "#<Fast::Regexp::MatchData #{to_s.inspect}>"
|
|
326
|
-
end
|
|
327
342
|
end
|
|
328
343
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fast_regexp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -11,7 +11,7 @@ cert_chain: []
|
|
|
11
11
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: 'Ruby bindings to rust/regex with a Fast::Regexp API: MatchData, sub/gsub,
|
|
14
|
-
===, =~, named captures
|
|
14
|
+
===, =~, & named captures.'
|
|
15
15
|
email:
|
|
16
16
|
- eric@ebj.dev
|
|
17
17
|
executables: []
|
|
@@ -33,6 +33,7 @@ licenses:
|
|
|
33
33
|
- MIT
|
|
34
34
|
metadata:
|
|
35
35
|
bug_tracker_uri: https://github.com/jetpks/fast_regexp/issues
|
|
36
|
+
changelog_uri: https://github.com/jetpks/fast_regexp/blob/main/CHANGELOG.md
|
|
36
37
|
homepage_uri: https://github.com/jetpks/fast_regexp
|
|
37
38
|
source_code_uri: https://github.com/jetpks/fast_regexp
|
|
38
39
|
rdoc_options: []
|