fast_regexp 0.6.2-aarch64-linux → 0.7.0-aarch64-linux
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/lib/fast_regexp/3.3/fast_regexp.so +0 -0
- data/lib/fast_regexp/3.4/fast_regexp.so +0 -0
- data/lib/fast_regexp/4.0/fast_regexp.so +0 -0
- data/lib/fast_regexp/version.rb +1 -1
- data/lib/fast_regexp.rb +92 -85
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb638b676baa678009503cd7a42a5a03befb73c00b79a72938480c74cd5a0538
|
|
4
|
+
data.tar.gz: 0033a9a765b873df3b9984fa138ebc534e9b3f0458d1a8018f71d3f5f768786e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d30c68136e478c8adcf061bd06d2df042e1af051bce51fc38076f960c8bb9d6eec0336c895dec3dec369401f1bb9207975a128298cd1ac03e2752f0a3281cb8
|
|
7
|
+
data.tar.gz: aad4a7176414ee857ed229bc0c4cafcfeba8e2ed93b6743c05e924c5d3c8559541cbc1849553831d475c35946ec5d4dc6f13b389f6b8f308fe88111e64e81d71
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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",
|
|
@@ -96,8 +168,9 @@ module Fast
|
|
|
96
168
|
|
|
97
169
|
def match(haystack)
|
|
98
170
|
haystack = coerce_string(haystack)
|
|
99
|
-
|
|
100
|
-
|
|
171
|
+
return @backend._native_match(haystack) if fast?
|
|
172
|
+
m = @backend.match(haystack)
|
|
173
|
+
m && MatchData.new(m)
|
|
101
174
|
end
|
|
102
175
|
|
|
103
176
|
def match?(haystack)
|
|
@@ -113,8 +186,10 @@ module Fast
|
|
|
113
186
|
# also returns bytes here for API consistency).
|
|
114
187
|
def =~(other)
|
|
115
188
|
return nil unless other.respond_to?(:to_str)
|
|
116
|
-
|
|
117
|
-
|
|
189
|
+
haystack = other.to_str
|
|
190
|
+
return @backend._native_find(haystack) if fast?
|
|
191
|
+
m = @backend.match(haystack)
|
|
192
|
+
m && m.byteoffset(0)[0]
|
|
118
193
|
end
|
|
119
194
|
|
|
120
195
|
def scan(haystack)
|
|
@@ -123,22 +198,17 @@ module Fast
|
|
|
123
198
|
|
|
124
199
|
def scan_matches(haystack)
|
|
125
200
|
haystack = coerce_string(haystack)
|
|
126
|
-
if fast?
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match, haystack) }
|
|
131
|
-
results
|
|
132
|
-
end
|
|
201
|
+
return @backend.scan_matches(haystack) if fast?
|
|
202
|
+
results = []
|
|
203
|
+
haystack.scan(@backend) { results << MatchData.new(::Regexp.last_match) }
|
|
204
|
+
results
|
|
133
205
|
end
|
|
134
206
|
|
|
135
207
|
def sub(haystack, replacement = nil, literal: false, &block)
|
|
136
208
|
haystack = coerce_string(haystack)
|
|
137
|
-
if
|
|
209
|
+
if block_given?
|
|
138
210
|
raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
|
|
139
|
-
|
|
140
|
-
return haystack.dup unless m
|
|
141
|
-
"#{m.pre_match}#{block.call(m)}#{m.post_match}"
|
|
211
|
+
fast? ? @backend._native_sub_block(haystack, &block) : stdlib_sub_with_block(haystack, &block)
|
|
142
212
|
else
|
|
143
213
|
raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
|
|
144
214
|
replacement = coerce_string(replacement)
|
|
@@ -152,9 +222,9 @@ module Fast
|
|
|
152
222
|
|
|
153
223
|
def gsub(haystack, replacement = nil, literal: false, &block)
|
|
154
224
|
haystack = coerce_string(haystack)
|
|
155
|
-
if
|
|
225
|
+
if block_given?
|
|
156
226
|
raise ArgumentError, "wrong number of arguments (given 2, expected 1 with block)" if replacement
|
|
157
|
-
fast? ?
|
|
227
|
+
fast? ? @backend._native_gsub_block(haystack, &block) : stdlib_gsub_with_block(haystack, &block)
|
|
158
228
|
else
|
|
159
229
|
raise ArgumentError, "wrong number of arguments (given 1, expected 2)" if replacement.nil?
|
|
160
230
|
replacement = coerce_string(replacement)
|
|
@@ -254,29 +324,14 @@ module Fast
|
|
|
254
324
|
end
|
|
255
325
|
end
|
|
256
326
|
|
|
257
|
-
#
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
matches = scan_matches(haystack)
|
|
262
|
-
return haystack.dup if matches.empty?
|
|
263
|
-
|
|
264
|
-
out = String.new(encoding: Encoding::UTF_8)
|
|
265
|
-
cursor = 0
|
|
266
|
-
matches.each do |m|
|
|
267
|
-
bs, be = m.byteoffset(0)
|
|
268
|
-
out << haystack.byteslice(cursor, bs - cursor) if bs > cursor
|
|
269
|
-
out << yield(m).to_s
|
|
270
|
-
cursor = be
|
|
271
|
-
end
|
|
272
|
-
out << haystack.byteslice(cursor, haystack.bytesize - cursor) if cursor < haystack.bytesize
|
|
273
|
-
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 }
|
|
274
331
|
end
|
|
275
332
|
|
|
276
|
-
# Stdlib path: String#gsub already does single-pass iterate-and-replace
|
|
277
|
-
# and sets $~ inside the block, so wrap the current ::MatchData and yield.
|
|
278
333
|
def stdlib_gsub_with_block(haystack)
|
|
279
|
-
haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match
|
|
334
|
+
haystack.gsub(@backend) { yield(MatchData.new(::Regexp.last_match)).to_s }
|
|
280
335
|
end
|
|
281
336
|
|
|
282
337
|
def coerce_string(value)
|
|
@@ -284,53 +339,5 @@ module Fast
|
|
|
284
339
|
return value.to_str if value.respond_to?(:to_str)
|
|
285
340
|
raise TypeError, "no implicit conversion of #{value.class} into String"
|
|
286
341
|
end
|
|
287
|
-
|
|
288
|
-
# Wraps either a Fast::Regexp::Native::MatchData or a stdlib ::MatchData
|
|
289
|
-
# so callers see one type regardless of which backend ran.
|
|
290
|
-
class MatchData
|
|
291
|
-
include Enumerable
|
|
292
|
-
|
|
293
|
-
attr_reader :backend, :string
|
|
294
|
-
|
|
295
|
-
def initialize(backend, haystack)
|
|
296
|
-
@backend = backend
|
|
297
|
-
@string = haystack
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def native? = @backend.is_a?(Fast::Regexp::Native::MatchData)
|
|
301
|
-
def stdlib? = !native?
|
|
302
|
-
|
|
303
|
-
def native = native? ? @backend : nil
|
|
304
|
-
def stdlib = stdlib? ? @backend : nil
|
|
305
|
-
|
|
306
|
-
def [](key) = @backend[key]
|
|
307
|
-
def to_a = @backend.to_a
|
|
308
|
-
def captures = @backend.captures
|
|
309
|
-
def named_captures = @backend.named_captures
|
|
310
|
-
def names = @backend.names
|
|
311
|
-
def size = @backend.size
|
|
312
|
-
alias_method :length, :size
|
|
313
|
-
def pre_match = @backend.pre_match
|
|
314
|
-
def post_match = @backend.post_match
|
|
315
|
-
def to_s = @backend.to_s
|
|
316
|
-
|
|
317
|
-
# Byte-based offsets. Both backends expose these (stdlib MatchData has
|
|
318
|
-
# `byteoffset` / `byte_begin` / `byte_end` since Ruby 3.2).
|
|
319
|
-
def byteoffset(key) = @backend.byteoffset(key)
|
|
320
|
-
def byte_begin(key) = @backend.byte_begin(key)
|
|
321
|
-
def byte_end(key) = @backend.byte_end(key)
|
|
322
|
-
|
|
323
|
-
def each(&block) = to_a.each(&block)
|
|
324
|
-
def values_at(*indices) = indices.map { |i| self[i] }
|
|
325
|
-
|
|
326
|
-
def ==(other)
|
|
327
|
-
other.is_a?(MatchData) && to_a == other.to_a && string == other.string
|
|
328
|
-
end
|
|
329
|
-
alias_method :eql?, :==
|
|
330
|
-
|
|
331
|
-
def hash = [to_a, string].hash
|
|
332
|
-
|
|
333
|
-
def inspect = "#<Fast::Regexp::MatchData #{to_s.inspect}>"
|
|
334
|
-
end
|
|
335
342
|
end
|
|
336
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: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Jacobs
|
|
@@ -9,10 +9,10 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-09-19 00:00:00.000000000 Z
|
|
13
13
|
dependencies: []
|
|
14
14
|
description: 'Ruby bindings to rust/regex with a Fast::Regexp API: MatchData, sub/gsub,
|
|
15
|
-
===, =~, named captures
|
|
15
|
+
===, =~, & named captures.'
|
|
16
16
|
email:
|
|
17
17
|
- eric@ebj.dev
|
|
18
18
|
executables: []
|
|
@@ -31,6 +31,7 @@ licenses:
|
|
|
31
31
|
- MIT
|
|
32
32
|
metadata:
|
|
33
33
|
bug_tracker_uri: https://github.com/jetpks/fast_regexp/issues
|
|
34
|
+
changelog_uri: https://github.com/jetpks/fast_regexp/blob/main/CHANGELOG.md
|
|
34
35
|
homepage_uri: https://github.com/jetpks/fast_regexp
|
|
35
36
|
source_code_uri: https://github.com/jetpks/fast_regexp
|
|
36
37
|
post_install_message:
|