Skip to content

Commit 0c52e5e

Browse files
committed
chore(release): v3.0.0
1 parent 1632514 commit 0c52e5e

8 files changed

Lines changed: 102 additions & 37 deletions

File tree

.github/workflows/ci.yml

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,27 @@ name: CI
22

33
on:
44
push:
5+
branches: [ master ]
56
pull_request:
6-
workflow_dispatch:
7-
# schedule:
8-
# - cron: '42 5 * * *'
7+
branches: [ master ]
98

109
jobs:
1110
test:
11+
runs-on: ubuntu-latest
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
ruby: [ '3.0' ]
16-
17-
runs-on: ubuntu-latest
18-
name: Ruby ${{matrix.ruby}}
19-
container: ruby:${{matrix.ruby}}
20-
15+
ruby: [ '3.0', '3.1', '3.2', '3.3', '3.4' ]
2116
steps:
22-
- uses: actions/checkout@v3
23-
24-
- name: Show ruby Version
25-
run: |
26-
ruby -v
27-
28-
- name: Install Modules
29-
run: ./bin/setup
30-
31-
- name: Compile
32-
run: rake compile
33-
34-
- name: Run tests
35-
run: rake spec
17+
- uses: actions/checkout@v4
18+
- uses: ruby/setup-ruby@v1
19+
with:
20+
ruby-version: ${{ matrix.ruby }}
21+
bundler-cache: true
22+
- name: Install dependencies
23+
run: bundle install --jobs 4 --retry 3
24+
- name: Compile native extension
25+
run: rake compile
26+
- name: Run specs
27+
run: rake spec
3628

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,27 @@
11
# 2.0.2 (August 10, 2023)
2+
# 2.0.2 (August 10, 2023)
3+
4+
# 3.0.0 (September 11, 2025)
5+
6+
## Features
7+
8+
- New `MultiStringReplace::Automaton` API to compile patterns once and reuse for faster repeated match/replace.
9+
10+
## Performance
11+
12+
- Faster replace path: cached Ruby method IDs, fewer API calls, preallocated buffers, and deferred string allocation for no-match fast path.
13+
- Optimized trie setup/clear and memory copies.
14+
15+
## Stability and build
16+
17+
- Fixed Bundler conflicts on Ruby 3.4+; Rakefile avoids dual-Bundler loading.
18+
- Added GC marking to prevent crashes with the new Automaton.
19+
- Safer behavior when replacement is missing; falls back to original substring.
20+
21+
## Tooling
22+
23+
- Benchmark script now supports `-f/--file`, `-n/--iters`, and `-A/--automaton` flags.
24+
- Added GitHub Actions CI across Ruby 3.0–3.4.
225

326
## Bug fixes:
427

Gemfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
multi_string_replace (2.0.2)
4+
multi_string_replace (3.0.0)
55

66
GEM
77
remote: https://rubygems.org/
@@ -36,4 +36,4 @@ DEPENDENCIES
3636
rspec (~> 3.0)
3737

3838
BUNDLED WITH
39-
2.3.27
39+
2.6.9

ext/multi_string_replace/ahocorasick.c

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,8 @@ VALUE aho_replace_text(struct ahocorasick * restrict aho, const char* data,
194194
struct aho_trie_node* travasal_node = NULL;
195195

196196
travasal_node = &(aho->trie.root);
197-
// Preallocate with input size; Ruby will grow if needed
198-
VALUE main_result = rb_str_buf_new((long)data_len);
199-
197+
// Defer allocation until first match; handle no-match fast path
198+
VALUE main_result = Qnil;
200199
unsigned long long last_concat_pos = 0;
201200

202201
for (i = 0; i < data_len; i++)
@@ -212,8 +211,11 @@ VALUE aho_replace_text(struct ahocorasick * restrict aho, const char* data,
212211
const int rlen = result->len;
213212
unsigned long long pos = i - rlen + 1;
214213

215-
// concatenate from last_concat_pos
216-
if (pos > last_concat_pos) {
214+
// On first match, allocate result and copy prefix if any
215+
if (NIL_P(main_result)) {
216+
main_result = rb_str_buf_new((long)data_len);
217+
if (pos > 0) rb_str_cat(main_result, &data[0], pos);
218+
} else if (pos > last_concat_pos) {
217219
rb_str_cat(main_result, &data[last_concat_pos], pos - last_concat_pos);
218220
}
219221

@@ -249,11 +251,15 @@ VALUE aho_replace_text(struct ahocorasick * restrict aho, const char* data,
249251
last_concat_pos = i + 1;
250252
}
251253

252-
if (last_concat_pos < data_len) {
253-
rb_str_cat(main_result, &data[last_concat_pos], (long)(data_len - last_concat_pos));
254+
if (NIL_P(main_result)) {
255+
// No matches; return a copy of input (preserves previous API behavior of returning a new String)
256+
return rb_str_new(data, (long)data_len);
257+
} else {
258+
if (last_concat_pos < data_len) {
259+
rb_str_cat(main_result, &data[last_concat_pos], (long)(data_len - last_concat_pos));
260+
}
261+
return main_result;
254262
}
255-
256-
return main_result;
257263
}
258264

259265
inline void aho_register_match_callback(VALUE rb_result_container, struct ahocorasick * restrict aho,
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
require 'mkmf'
2-
create_header
3-
create_makefile 'multi_string_replace/multi_string_replace'
2+
3+
# Optional: allow users to tweak optimization flags
4+
optflags = ENV['MSR_OPTFLAGS'] || '-O3 -fno-strict-aliasing'
5+
warnflags = ENV['MSR_WARNFLAGS']
6+
7+
with_cflags(optflags) do
8+
# avoid treating warnings as errors across diverse compilers
9+
with_werror(false) do
10+
create_header
11+
create_makefile 'multi_string_replace/multi_string_replace'
12+
end
13+
end

ext/multi_string_replace/multi_string_replace.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ VALUE multi_string_replace(VALUE self, VALUE body, VALUE replace)
186186
VALUE replace_values = rb_funcall(replace, id_values, 0);
187187
long size = RARRAY_LEN(keys);
188188

189+
if (size == 0) {
190+
// Nothing to replace; return a copy of input (preserves API semantics)
191+
return rb_str_dup(body);
192+
}
193+
189194
long value_sizes[size];
190195
char *values[size];
191196
VALUE ruby_val[size];
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module MultiStringReplace
2-
VERSION = "2.0.2"
2+
VERSION = "3.0.0"
33
end

spec/automaton_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'spec_helper'
2+
RSpec.describe MultiStringReplace::Automaton do
3+
it 'matches with compiled automaton' do
4+
ac = described_class.new(['brown', 'fox'])
5+
res = ac.match('The quick brown fox')
6+
expect(res).to be_a(Hash)
7+
# ids 0 and 1 should exist with positions
8+
expect(res[0]).to include(10) # 'brown'
9+
expect(res[1]).to include(16) # 'fox'
10+
end
11+
12+
it 'replaces with compiled automaton' do
13+
ac = described_class.new(['brown', 'fox'])
14+
out = ac.replace('The quick brown fox', { 'brown' => 'black', 'fox' => 'wolf' })
15+
expect(out).to eq('The quick black wolf')
16+
end
17+
18+
it 'falls back to original when no replacement provided' do
19+
ac = described_class.new(['brown'])
20+
out = ac.replace('brown', {})
21+
expect(out).to eq('brown')
22+
end
23+
24+
it 'supports proc values' do
25+
ac = described_class.new(['x'])
26+
out = ac.replace('xx', { 'x' => ->(s, e) { 'y' } })
27+
expect(out).to eq('yy')
28+
end
29+
end

0 commit comments

Comments
 (0)