sin_fast_blank 4.0.1-java → 5.0.0-java

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4e851d2fe91cf3500ea48e4b3f6b2b7f7102220f5cdd749d2d0f40ce771d6e8
4
- data.tar.gz: a43c4b3688dfccd11fd98014eb9504c6da4063b94093462fdf3cd0cac9652c74
3
+ metadata.gz: 34c933a31a4b5f14ef54f92a544abfef3a09c2463e095fcca3542c6c1abc562a
4
+ data.tar.gz: ebade8139e66100f82cd361591c57e5e53d03d5804258a352d599a9bea87d923
5
5
  SHA512:
6
- metadata.gz: dcb74483cca8c2a4af589d2f1ce7ac35babb1fadbf9b0a5a46c42e8fa22c29ecc7db57f54914360b6546d0ec0bb6b02cf9ca2e3757e35f4170e35a5278c59b8d
7
- data.tar.gz: 60268c8edccd06b9c83d6304e06a7cec3688f32f6f0bd67fd5fd18fdabbceb547ebe8315ef95a0e5ef1f6f52dabd8313cb31d20929f655011197ce842ac7e83e
6
+ metadata.gz: 12adaa05f6ae256d519e0ea822e78aaf9a82466eee457837ace600d34eb3b1f68d52d9a3741ded095645fead9636db2942f9de341e92f0d26165c2432984db22
7
+ data.tar.gz: 36133336ea316b3fcb795e5565f38a9f4c53b666aff85f9212f96085506dbb7229c2ff255b9115381ef09e91d830cc477a747854fe0017df1080185619ed9a97
data/CHANGELOG.md ADDED
@@ -0,0 +1,215 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [5.0.0] - 2026-08-10
9
+
10
+ ### Added
11
+
12
+ - Add Ractor support
13
+ - Add SSE2 SIMD support on mswin builds
14
+
15
+ ### Changed
16
+
17
+ - Improve SIMD performance with overlapped final chunks and continued ASCII scanning
18
+ - Skip the dispatch pointer for short strings
19
+ - Simplify native extension loading
20
+ - Refactor Java extension
21
+ - Add JRuby build and test script
22
+ - Build JRuby extension against the oldest supported JRuby
23
+ - Probe compiler flags before adding them
24
+ - Drop the unused 32-bit ARM NEON flag
25
+ - Make test task depend on compile
26
+ - Minimize gem version constraints
27
+ - Remove unneeded json gem dependency
28
+ - Update tests
29
+ - Improve test CI
30
+ - Improve lint CI
31
+ - Require tests to pass before releasing
32
+ - Upgrade actions/checkout to v7
33
+ - Improve benchmark
34
+ - Update .rubocop.yml
35
+ - Update .gitignore
36
+ - Update README.md
37
+ - Update sponsor links
38
+ - Update compiled jar
39
+
40
+ ### Fixed
41
+
42
+ - Fix `blank?` to match per-encoding space tables
43
+ - Fix `blank?` to raise only when Ruby calls the string broken
44
+ - Fix `ascii_blank?` to answer instead of raising on invalid byte sequences
45
+ - Fix AVX2 selection to use runtime CPU detection
46
+ - Fix Java extension loading without `JRuby::Util.load_ext`
47
+ - Fix packaged gem to include files under lib
48
+ - Fix C extension install path
49
+ - Fix stray compiled jar getting into the packaged gem
50
+ - Fix benchmark
51
+
52
+ ## [4.0.1] - 2026-02-22
53
+
54
+ ### Changed
55
+
56
+ - Improve SIMD performance with range-based approach and add `ascii_blank?` SIMD support
57
+ - Refactor Java extension
58
+ - Format C and Java code
59
+ - Add C and Java lint CI jobs
60
+ - Upgrade actions/checkout to v6
61
+ - Pin gem versions
62
+ - Add .clang-format configuration
63
+ - Improve benchmark
64
+ - Update README.md
65
+ - Update .gitignore
66
+
67
+ ### Fixed
68
+
69
+ - Fix Java extension compatibility checks
70
+ - Fix C extension ARM NEON and pointer safety issues
71
+ - Fix benchmark
72
+ - Fix clang-format lint path
73
+
74
+ ## [4.0.0] - 2025-08-13
75
+
76
+ ### Changed
77
+
78
+ - Improve performance
79
+ - Update tests
80
+ - Update benchmark
81
+ - Update .rubocop.yml
82
+ - Improve test CI
83
+ - Improve lint CI
84
+ - Update .gitignore
85
+ - Update README.md
86
+
87
+ ## [3.1.1] - 2025-03-18
88
+
89
+ ### Fixed
90
+
91
+ - Fix test CI
92
+
93
+ ### Changed
94
+
95
+ - Update summary
96
+
97
+ ## [3.1.0] - 2025-03-12
98
+
99
+ ### Changed
100
+
101
+ - Refactor implementation
102
+ - Update summary
103
+
104
+ ### Fixed
105
+
106
+ - Fix test
107
+
108
+ ## [3.0.0] - 2025-03-05
109
+
110
+ ### Changed
111
+
112
+ - Rename `String#blank_as?` to `String#blank?`
113
+ - Rename `String#blank?` to `String#ascii_blank?`
114
+ - Update tests
115
+ - Update benchmark
116
+ - Update README.md
117
+
118
+ ## [2.2.1] - 2025-03-03
119
+
120
+ ### Changed
121
+
122
+ - Update tests
123
+ - Update summary
124
+
125
+ ### Fixed
126
+
127
+ - Fix benchmark
128
+
129
+ ## [2.2.0] - 2025-03-02
130
+
131
+ ### Added
132
+
133
+ - Add .editorconfig
134
+
135
+ ### Changed
136
+
137
+ - Improve performance
138
+ - Update benchmark
139
+ - Update .rubocop.yml
140
+ - Update summary
141
+ - Update README.md
142
+
143
+ ## [2.1.0] - 2025-01-17
144
+
145
+ ### Changed
146
+
147
+ - Improve gemspec
148
+ - Improve test CI
149
+
150
+ ## [2.0.0] - 2025-01-06
151
+
152
+ ### Added
153
+
154
+ - Add bin
155
+
156
+ ### Changed
157
+
158
+ - Improve implementation
159
+ - Improve tests
160
+ - Update .gitignore
161
+ - Update Gemfile
162
+ - Update gemspec
163
+ - Improve CI
164
+ - Improve documents
165
+
166
+ ### Removed
167
+
168
+ - Drop runtime support for Ruby 2.2 and below
169
+
170
+ ## [1.0.1] - 2021-08-16/2021-12-06
171
+
172
+ ### Added
173
+
174
+ - Support JRuby
175
+
176
+ ### Changed
177
+
178
+ - Avoid warnings if redefining blank?
179
+
180
+ ## [1.0.0] - 2015-08-03
181
+
182
+ ### Added
183
+
184
+ - Ruby 2.2 support ([@tjschuck](https://github.com/tjschuck) — [#9](https://github.com/SamSaffron/fast_blank/pull/9))
185
+
186
+ ## [0.0.2] - 2013-11-20
187
+
188
+ ### Changed
189
+
190
+ - Unrolled internal loop to improve perf ([@tmm1](https://github.com/tmm1) — [#2](https://github.com/SamSaffron/fast_blank/pull/2))
191
+
192
+ ### Removed
193
+
194
+ - Removed rake dependency ([@tmm1](https://github.com/tmm1) — [#2](https://github.com/SamSaffron/fast_blank/pull/2))
195
+
196
+ ## [0.0.1] - 2013-04-07
197
+
198
+ ### Added
199
+
200
+ - Initial release
201
+
202
+ [5.0.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v4.0.1...v5.0.0
203
+ [4.0.1]: https://github.com/cadenza-tech/sin_fast_blank/compare/v4.0.0...v4.0.1
204
+ [4.0.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v3.1.1...v4.0.0
205
+ [3.1.1]: https://github.com/cadenza-tech/sin_fast_blank/compare/v3.1.0...v3.1.1
206
+ [3.1.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v3.0.0...v3.1.0
207
+ [3.0.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v2.2.1...v3.0.0
208
+ [2.2.1]: https://github.com/cadenza-tech/sin_fast_blank/compare/v2.2.0...v2.2.1
209
+ [2.2.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v2.1.0...v2.2.0
210
+ [2.1.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v2.0.0...v2.1.0
211
+ [2.0.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/v1.0.1...v2.0.0
212
+ [1.0.1]: https://github.com/cadenza-tech/sin_fast_blank/compare/1.0.0...v1.0.1
213
+ [1.0.0]: https://github.com/cadenza-tech/sin_fast_blank/compare/0.0.2...1.0.0
214
+ [0.0.2]: https://github.com/cadenza-tech/sin_fast_blank/compare/0.0.1...0.0.2
215
+ [0.0.1]: https://github.com/cadenza-tech/sin_fast_blank/releases/tag/0.0.1
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2006 Steve Sloan
4
+ Copyright (c) 2013 Sam Saffron
5
+ Copyright (c) 2025 Masahiro
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,307 @@
1
+ # SinFastBlank
2
+
3
+ [![License](https://img.shields.io/github/license/cadenza-tech/sin_fast_blank?label=License&labelColor=343B42&color=blue)](https://github.com/cadenza-tech/sin_fast_blank/blob/main/LICENSE.txt) [![Tag](https://img.shields.io/github/tag/cadenza-tech/sin_fast_blank?label=Tag&logo=github&labelColor=343B42&color=2EBC4F)](https://github.com/cadenza-tech/sin_fast_blank/blob/main/CHANGELOG.md) [![Release](https://github.com/cadenza-tech/sin_fast_blank/actions/workflows/release.yml/badge.svg)](https://github.com/cadenza-tech/sin_fast_blank/actions?query=workflow%3Arelease) [![Test](https://github.com/cadenza-tech/sin_fast_blank/actions/workflows/test.yml/badge.svg)](https://github.com/cadenza-tech/sin_fast_blank/actions?query=workflow%3Atest) [![Lint](https://github.com/cadenza-tech/sin_fast_blank/actions/workflows/lint.yml/badge.svg)](https://github.com/cadenza-tech/sin_fast_blank/actions?query=workflow%3Alint)
4
+
5
+ Check for blank string faster than FastBlank or ActiveSupport.
6
+
7
+ Forked from [FastBlank](https://github.com/SamSaffron/fast_blank).
8
+
9
+ - [Installation](#installation)
10
+ - [Usage](#usage)
11
+ - [String#blank?](#stringblank)
12
+ - [String#ascii\_blank?](#stringascii_blank)
13
+ - [Benchmark](#benchmark)
14
+ - [Development](#development)
15
+ - [Building for JRuby](#building-for-jruby)
16
+ - [Changelog](#changelog)
17
+ - [Contributing](#contributing)
18
+ - [License](#license)
19
+ - [Code of Conduct](#code-of-conduct)
20
+ - [Sponsor](#sponsor)
21
+
22
+ ## Installation
23
+
24
+ Install the gem and add to the application's Gemfile by executing:
25
+
26
+ ```bash
27
+ bundle add sin_fast_blank
28
+ ```
29
+
30
+ If bundler is not being used to manage dependencies, install the gem by executing:
31
+
32
+ ```bash
33
+ gem install sin_fast_blank
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ Both methods can be called inside non-main Ractors on CRuby 3.0+.
39
+
40
+ ActiveSupport defines `String#blank?` on String itself in every version, so whichever of the two is loaded last replaces the other: require `sin_fast_blank` after ActiveSupport. `require 'active_support'` on its own does not reach that definition, while `active_support/all`, `active_support/core_ext/object`, `active_support/core_ext/object/blank` and `rails/all` do. `String.instance_method(:blank?).source_location` is `nil` while the extension is the one in place. `String#ascii_blank?` has no ActiveSupport counterpart and is never replaced.
41
+
42
+ ### String#blank?
43
+
44
+ SinFastBlank's String#blank? is compatible with ActiveSupport's String#blank?.
45
+
46
+ ```ruby
47
+ require 'sin_fast_blank'
48
+
49
+ ''.blank? # => true
50
+ ' '.blank? # => true
51
+ ' '.blank? # => true
52
+ "\t".blank? # => true
53
+ "\r".blank? # => true
54
+ "\n".blank? # => true
55
+ "\r\n".blank? # => true
56
+ "\r\n\v\f\r\s\t".blank? # => true
57
+ 'abc'.blank? # => false
58
+ ' abc '.blank? # => false
59
+ ```
60
+
61
+ The check is encoding-aware: codepoints match the same per-encoding `[[:space:]]` tables that ActiveSupport's regexp uses. This holds on CRuby and JRuby. TruffleRuby's regexp engine consults a different table, so a handful of single-byte cases disagree: it reports `0x85` and `0xA0` as blank in encodings whose ctype table does not, such as `ASCII-8BIT`.
62
+
63
+ Dummy encodings are the one place the two part ways. ActiveSupport cannot build its regexp for a string tagged `ISO-2022-JP`, `UTF-16` or `UTF-7`, so it raises `RegexpError` or `Encoding::ConverterNotFoundError`; `String#blank?` scans the bytes and answers.
64
+
65
+ SinFastBlank scans left to right and returns as soon as the result is decided, so it can answer before reaching an invalid byte sequence that ActiveSupport's whole-string regexp would trip over: `"a\xFF".blank?` returns `false` where ActiveSupport raises `ArgumentError`. Once the scan does reach one, `blank?` matches ActiveSupport exactly: it raises `ArgumentError` for a string Ruby itself calls broken, and answers `false` for one whose bytes Ruby's own transcoder produced but its scanner rejects (`'À'.encode('Big5-HKSCS')`), which ActiveSupport does not treat as an error either.
66
+
67
+ One more case parts ways, this time on the receiver rather than its bytes: ActiveSupport shortcuts on `empty?` before matching its regexp, so a String that overrides `empty?` to return true is blank there, where `String#blank?` reads the bytes and answers on what they hold. ActiveSupport documents that call as a speedup for empty strings rather than a hook, and `ActiveSupport::SafeBuffer` does not override it.
68
+
69
+ ### String#ascii_blank?
70
+
71
+ ```ruby
72
+ require 'sin_fast_blank'
73
+
74
+ ''.ascii_blank? # => true
75
+ ' '.ascii_blank? # => true
76
+ "\t".ascii_blank? # => true
77
+ "\r".ascii_blank? # => true
78
+ "\n".ascii_blank? # => true
79
+ "\r\n".ascii_blank? # => true
80
+ "\r\n\v\f\r\s\t".ascii_blank? # => true
81
+ 'abc'.ascii_blank? # => false
82
+ ' abc '.ascii_blank? # => false
83
+ ```
84
+
85
+ Only ASCII whitespace counts, so `String#ascii_blank?` never raises: bytes that form no character are not ASCII whitespace either, and `"\xFF".ascii_blank?` simply returns `false`.
86
+
87
+ ## Benchmark
88
+
89
+ SinFastBlank's String#blank? is about 1.3-16.3x faster than FastBlank's String#blank_as? and about 1.2-23.0x faster than ActiveSupport's String#blank?. SinFastBlank's String#ascii_blank? is about 1.2-23.2x faster than FastBlank's String#blank?. At string length 123 the measurement does not separate String#blank? from FastBlank's String#blank_as?, so both are reported as Fastest there.
90
+
91
+ Additionally, this gem allocates no strings during the check, making it less of a burden on the GC.
92
+
93
+ Each string length is reported as two tables because the groups answer different questions: the ActiveSupport-compatible group calls U+3000 and U+00A0 blank, while the ASCII-only pair returns false for both. Ranking them together would compare methods that never did the same work.
94
+
95
+ ```bash
96
+ $ bundle exec rake benchmark
97
+ Benchmarking string length: 0...
98
+ Benchmarking string length: 8...
99
+ Benchmarking string length: 43...
100
+ Benchmarking string length: 123...
101
+ Benchmarking string length: 127...
102
+ Benchmarking string length: 238...
103
+
104
+ +-----------------------------------------------------------------------+
105
+ | Benchmark Result (String Length: 0, ActiveSupport-compatible) |
106
+ +------------------------+----------------------+--------+--------------+
107
+ | Name | Iteration Per Second | Error | Speed Ratio |
108
+ +------------------------+----------------------+--------+--------------+
109
+ | SinFastBlank - blank? | 189268656.7 | ±0.48% | Fastest |
110
+ | Scratch - blank_e? | 159245632.6 | ±0.42% | 1.2x slower |
111
+ | Scratch - blank_g? | 159177797.8 | ±0.51% | 1.2x slower |
112
+ | ActiveSupport - blank? | 159005067.3 | ±1.02% | 1.2x slower |
113
+ | Scratch - blank_f? | 158579113.9 | ±1.72% | 1.2x slower |
114
+ | Scratch - blank_h? | 158312815.2 | ±0.38% | 1.2x slower |
115
+ | FastBlank - blank_as? | 146149656.1 | ±2.58% | 1.3x slower |
116
+ | Scratch - blank_a? | 24506273.5 | ±0.37% | 7.7x slower |
117
+ | Scratch - blank_c? | 11745600.3 | ±0.23% | 16.1x slower |
118
+ | Scratch - blank_b? | 10570972.1 | ±0.56% | 17.9x slower |
119
+ | Scratch - blank_d? | 4905509.6 | ±0.33% | 38.6x slower |
120
+ +------------------------+----------------------+--------+--------------+
121
+
122
+ +---------------------------------------------------------------------------+
123
+ | Benchmark Result (String Length: 0, ASCII-only) |
124
+ +-----------------------------+----------------------+--------+-------------+
125
+ | Name | Iteration Per Second | Error | Speed Ratio |
126
+ +-----------------------------+----------------------+--------+-------------+
127
+ | SinFastBlank - ascii_blank? | 203193300.1 | ±1.09% | Fastest |
128
+ | FastBlank - blank? | 147055633.6 | ±0.60% | 1.4x slower |
129
+ +-----------------------------+----------------------+--------+-------------+
130
+
131
+ +-----------------------------------------------------------------------+
132
+ | Benchmark Result (String Length: 8, ActiveSupport-compatible) |
133
+ +------------------------+----------------------+--------+--------------+
134
+ | Name | Iteration Per Second | Error | Speed Ratio |
135
+ +------------------------+----------------------+--------+--------------+
136
+ | SinFastBlank - blank? | 77064598.2 | ±0.31% | Fastest |
137
+ | FastBlank - blank_as? | 27870007.6 | ±0.62% | 2.8x slower |
138
+ | Scratch - blank_c? | 14118003.3 | ±0.26% | 5.5x slower |
139
+ | ActiveSupport - blank? | 13698901.0 | ±0.52% | 5.6x slower |
140
+ | Scratch - blank_g? | 13635284.9 | ±0.20% | 5.7x slower |
141
+ | Scratch - blank_b? | 10198011.1 | ±2.15% | 7.6x slower |
142
+ | Scratch - blank_f? | 10142164.2 | ±0.32% | 7.6x slower |
143
+ | Scratch - blank_a? | 6783807.1 | ±0.98% | 11.4x slower |
144
+ | Scratch - blank_e? | 6521011.0 | ±0.29% | 11.8x slower |
145
+ | Scratch - blank_d? | 5215856.9 | ±1.46% | 14.8x slower |
146
+ | Scratch - blank_h? | 5166323.2 | ±1.75% | 14.9x slower |
147
+ +------------------------+----------------------+--------+--------------+
148
+
149
+ +---------------------------------------------------------------------------+
150
+ | Benchmark Result (String Length: 8, ASCII-only) |
151
+ +-----------------------------+----------------------+--------+-------------+
152
+ | Name | Iteration Per Second | Error | Speed Ratio |
153
+ +-----------------------------+----------------------+--------+-------------+
154
+ | SinFastBlank - ascii_blank? | 89085855.6 | ±0.19% | Fastest |
155
+ | FastBlank - blank? | 31180410.5 | ±0.32% | 2.9x slower |
156
+ +-----------------------------+----------------------+--------+-------------+
157
+
158
+ +-----------------------------------------------------------------------+
159
+ | Benchmark Result (String Length: 43, ActiveSupport-compatible) |
160
+ +------------------------+----------------------+--------+--------------+
161
+ | Name | Iteration Per Second | Error | Speed Ratio |
162
+ +------------------------+----------------------+--------+--------------+
163
+ | SinFastBlank - blank? | 80570773.4 | ±0.17% | Fastest |
164
+ | FastBlank - blank_as? | 4951349.2 | ±0.21% | 16.3x slower |
165
+ | Scratch - blank_b? | 4572305.5 | ±1.00% | 17.6x slower |
166
+ | Scratch - blank_f? | 4437925.4 | ±1.15% | 18.2x slower |
167
+ | Scratch - blank_g? | 3531002.2 | ±0.84% | 22.8x slower |
168
+ | Scratch - blank_c? | 3523113.8 | ±1.96% | 22.9x slower |
169
+ | ActiveSupport - blank? | 3510419.5 | ±2.05% | 23.0x slower |
170
+ | Scratch - blank_h? | 2976077.1 | ±0.52% | 27.1x slower |
171
+ | Scratch - blank_d? | 2922150.2 | ±2.47% | 27.6x slower |
172
+ | Scratch - blank_a? | 2275385.3 | ±1.61% | 35.4x slower |
173
+ | Scratch - blank_e? | 2257794.6 | ±1.32% | 35.7x slower |
174
+ +------------------------+----------------------+--------+--------------+
175
+
176
+ +----------------------------------------------------------------------------+
177
+ | Benchmark Result (String Length: 43, ASCII-only) |
178
+ +-----------------------------+----------------------+--------+--------------+
179
+ | Name | Iteration Per Second | Error | Speed Ratio |
180
+ +-----------------------------+----------------------+--------+--------------+
181
+ | SinFastBlank - ascii_blank? | 105229741.2 | ±0.55% | Fastest |
182
+ | FastBlank - blank? | 4543568.4 | ±3.49% | 23.2x slower |
183
+ +-----------------------------+----------------------+--------+--------------+
184
+
185
+ +-----------------------------------------------------------------------+
186
+ | Benchmark Result (String Length: 123, ActiveSupport-compatible) |
187
+ +------------------------+----------------------+--------+--------------+
188
+ | Name | Iteration Per Second | Error | Speed Ratio |
189
+ +------------------------+----------------------+--------+--------------+
190
+ | SinFastBlank - blank? | 96936949.7 | ±3.02% | Fastest |
191
+ | FastBlank - blank_as? | 93386361.9 | ±0.75% | Fastest |
192
+ | Scratch - blank_a? | 26971080.4 | ±2.73% | 3.6x slower |
193
+ | Scratch - blank_e? | 25639570.3 | ±0.18% | 3.8x slower |
194
+ | Scratch - blank_c? | 18687829.5 | ±0.25% | 5.2x slower |
195
+ | Scratch - blank_g? | 17894413.2 | ±0.33% | 5.4x slower |
196
+ | ActiveSupport - blank? | 16714938.4 | ±4.62% | 5.8x slower |
197
+ | Scratch - blank_d? | 9048125.6 | ±0.37% | 10.7x slower |
198
+ | Scratch - blank_h? | 8987475.0 | ±0.68% | 10.8x slower |
199
+ | Scratch - blank_b? | 5352834.6 | ±0.37% | 18.1x slower |
200
+ | Scratch - blank_f? | 5191973.0 | ±0.67% | 18.7x slower |
201
+ +------------------------+----------------------+--------+--------------+
202
+
203
+ +---------------------------------------------------------------------------+
204
+ | Benchmark Result (String Length: 123, ASCII-only) |
205
+ +-----------------------------+----------------------+--------+-------------+
206
+ | Name | Iteration Per Second | Error | Speed Ratio |
207
+ +-----------------------------+----------------------+--------+-------------+
208
+ | SinFastBlank - ascii_blank? | 120606725.9 | ±0.94% | Fastest |
209
+ | FastBlank - blank? | 102556412.3 | ±0.20% | 1.2x slower |
210
+ +-----------------------------+----------------------+--------+-------------+
211
+
212
+ +-----------------------------------------------------------------------+
213
+ | Benchmark Result (String Length: 127, ActiveSupport-compatible) |
214
+ +------------------------+----------------------+--------+--------------+
215
+ | Name | Iteration Per Second | Error | Speed Ratio |
216
+ +------------------------+----------------------+--------+--------------+
217
+ | SinFastBlank - blank? | 80551912.4 | ±0.35% | Fastest |
218
+ | FastBlank - blank_as? | 42206464.1 | ±0.96% | 1.9x slower |
219
+ | Scratch - blank_c? | 12828245.1 | ±0.23% | 6.3x slower |
220
+ | ActiveSupport - blank? | 12485897.9 | ±0.52% | 6.5x slower |
221
+ | Scratch - blank_g? | 12442303.6 | ±0.23% | 6.5x slower |
222
+ | Scratch - blank_a? | 10419013.7 | ±0.16% | 7.7x slower |
223
+ | Scratch - blank_e? | 10168685.4 | ±0.27% | 7.9x slower |
224
+ | Scratch - blank_d? | 7442964.2 | ±0.58% | 10.8x slower |
225
+ | Scratch - blank_h? | 7397822.4 | ±0.27% | 10.9x slower |
226
+ | Scratch - blank_b? | 5286423.4 | ±0.33% | 15.2x slower |
227
+ | Scratch - blank_f? | 5207047.6 | ±0.39% | 15.5x slower |
228
+ +------------------------+----------------------+--------+--------------+
229
+
230
+ +---------------------------------------------------------------------------+
231
+ | Benchmark Result (String Length: 127, ASCII-only) |
232
+ +-----------------------------+----------------------+--------+-------------+
233
+ | Name | Iteration Per Second | Error | Speed Ratio |
234
+ +-----------------------------+----------------------+--------+-------------+
235
+ | SinFastBlank - ascii_blank? | 120881119.0 | ±0.24% | Fastest |
236
+ | FastBlank - blank? | 45716325.1 | ±0.32% | 2.6x slower |
237
+ +-----------------------------+----------------------+--------+-------------+
238
+
239
+ +-----------------------------------------------------------------------+
240
+ | Benchmark Result (String Length: 238, ActiveSupport-compatible) |
241
+ +------------------------+----------------------+--------+--------------+
242
+ | Name | Iteration Per Second | Error | Speed Ratio |
243
+ +------------------------+----------------------+--------+--------------+
244
+ | SinFastBlank - blank? | 80596263.4 | ±0.20% | Fastest |
245
+ | FastBlank - blank_as? | 42305937.1 | ±0.20% | 1.9x slower |
246
+ | Scratch - blank_c? | 11596439.7 | ±0.33% | 7.0x slower |
247
+ | ActiveSupport - blank? | 11307320.4 | ±0.51% | 7.1x slower |
248
+ | Scratch - blank_g? | 11288221.5 | ±0.24% | 7.1x slower |
249
+ | Scratch - blank_a? | 9202475.9 | ±0.26% | 8.8x slower |
250
+ | Scratch - blank_e? | 9013830.2 | ±0.28% | 8.9x slower |
251
+ | Scratch - blank_d? | 7162431.6 | ±0.57% | 11.3x slower |
252
+ | Scratch - blank_h? | 7119191.1 | ±1.11% | 11.3x slower |
253
+ | Scratch - blank_b? | 3924922.1 | ±0.78% | 20.5x slower |
254
+ | Scratch - blank_f? | 3859441.2 | ±0.38% | 20.9x slower |
255
+ +------------------------+----------------------+--------+--------------+
256
+
257
+ +---------------------------------------------------------------------------+
258
+ | Benchmark Result (String Length: 238, ASCII-only) |
259
+ +-----------------------------+----------------------+--------+-------------+
260
+ | Name | Iteration Per Second | Error | Speed Ratio |
261
+ +-----------------------------+----------------------+--------+-------------+
262
+ | SinFastBlank - ascii_blank? | 120877343.1 | ±0.18% | Fastest |
263
+ | FastBlank - blank? | 45726024.3 | ±0.20% | 2.6x slower |
264
+ +-----------------------------+----------------------+--------+-------------+
265
+ ```
266
+
267
+ Each row's error is how much its own measurement moved within that run. Across runs the whole machine drifts further than that, so the ratios above move by a few tenths from one run to the next.
268
+
269
+ The error is also what decides the Speed Ratio column: a row is labelled Fastest when its gap to the top row is narrower than the two errors together, because a gap that small is the measurement failing to separate them rather than a difference it found. More than one row can therefore read Fastest.
270
+
271
+ Performance depends not only on the string length but also on its content.
272
+
273
+ The benchmark was executed in the following environment:
274
+
275
+ - Ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin25]
276
+ - FastBlank 1.0.1
277
+ - ActiveSupport 8.1.3.1
278
+
279
+ ## Development
280
+
281
+ ### Building for JRuby
282
+
283
+ To build the Java extension and run tests for JRuby support:
284
+
285
+ ```bash
286
+ ./script/jruby_build_and_test.sh
287
+ ```
288
+
289
+ ## Changelog
290
+
291
+ See [CHANGELOG.md](https://github.com/cadenza-tech/sin_fast_blank/blob/main/CHANGELOG.md).
292
+
293
+ ## Contributing
294
+
295
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cadenza-tech/sin_fast_blank. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/cadenza-tech/sin_fast_blank/blob/main/CODE_OF_CONDUCT.md).
296
+
297
+ ## License
298
+
299
+ The gem is available as open source under the terms of the [MIT License](https://github.com/cadenza-tech/sin_fast_blank/blob/main/LICENSE.txt).
300
+
301
+ ## Code of Conduct
302
+
303
+ Everyone interacting in the SinFastBlank project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/cadenza-tech/sin_fast_blank/blob/main/CODE_OF_CONDUCT.md).
304
+
305
+ ## Sponsor
306
+
307
+ You can sponsor this project on [GitHub Sponsors](https://github.com/sponsors/cadenza-tech).
data/Rakefile ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+ require 'rubocop/rake_task'
6
+ require 'rubygems/package_task'
7
+
8
+ gemspec = Gem::Specification.load("#{__dir__}/sin_fast_blank.gemspec")
9
+
10
+ if RUBY_ENGINE == 'jruby'
11
+ require 'rake/javaextensiontask'
12
+
13
+ Rake::JavaExtensionTask.new('sin_fast_blank', gemspec) do |task|
14
+ task.ext_dir = 'ext/java'
15
+ task.source_version = '1.8'
16
+ task.target_version = '1.8'
17
+ end
18
+ else
19
+ require 'rake/extensiontask'
20
+
21
+ Rake::ExtensionTask.new('sin_fast_blank', gemspec) do |task|
22
+ task.lib_dir = 'lib/sin_fast_blank'
23
+ end
24
+ end
25
+
26
+ Gem::PackageTask.new(gemspec)
27
+
28
+ Rake::TestTask.new(:test) do |task|
29
+ task.pattern = 'test/**/test_*.rb'
30
+ end
31
+
32
+ # Without this the suite happily runs against a stale binary, so a changed extension looks like it passed.
33
+ Rake::Task[:test].enhance([:compile])
34
+
35
+ RuboCop::RakeTask.new(:rubocop) do |task|
36
+ task.options = ['--format', ENV['RUBOCOP_FORMAT']] if ENV['RUBOCOP_FORMAT']
37
+ end
38
+
39
+ task benchmark: [:compile] do
40
+ RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && !RubyVM::YJIT.enabled?
41
+
42
+ require_relative 'script/blank_benchmark'
43
+
44
+ BlankBenchmark.run
45
+ end
@@ -8,9 +8,14 @@ import org.jruby.runtime.ThreadContext;
8
8
  import org.jruby.runtime.builtin.IRubyObject;
9
9
  import org.jruby.runtime.load.Library;
10
10
  import org.jruby.util.ByteList;
11
- import org.jruby.util.io.EncodingUtils;
11
+ import org.jruby.util.StringSupport;
12
12
 
13
13
  public class SinFastBlankLibrary implements Library {
14
+ private static final int ASCII_WS_RANGE_MIN = 0x09;
15
+ private static final int ASCII_WS_RANGE_MAX = 0x0d;
16
+ private static final int ASCII_WS_SPACE = 0x20;
17
+ private static final int MAX_CTYPE_CODEPOINT = 0xFF;
18
+
14
19
  @Override
15
20
  public void load(Ruby runtime, boolean wrap) {
16
21
  runtime.getString().defineAnnotatedMethods(SinFastBlankLibrary.class);
@@ -33,7 +38,7 @@ public class SinFastBlankLibrary implements Library {
33
38
  for (int i = s; i < e; i++) {
34
39
  byte c = bytes[i];
35
40
  if (c < 0) {
36
- return blankUnicodeSlow(context, bytes, i, e, enc);
41
+ return blankMixed(context, str, bytes, i, e, enc);
37
42
  }
38
43
  if (!isAsciiBlank(c)) {
39
44
  return context.fals;
@@ -42,11 +47,11 @@ public class SinFastBlankLibrary implements Library {
42
47
  return context.tru;
43
48
  }
44
49
 
45
- return blankUnicodeSlow(context, bytes, s, e, enc);
50
+ return blankUnicodeSlow(context, str, bytes, s, e, enc);
46
51
  }
47
52
 
48
53
  private static boolean isAsciiBlank(byte c) {
49
- return (c >= 0x09 && c <= 0x0d) || c == 0x20;
54
+ return (c >= ASCII_WS_RANGE_MIN && c <= ASCII_WS_RANGE_MAX) || c == ASCII_WS_SPACE;
50
55
  }
51
56
 
52
57
  private static boolean isUnicodeBlank(int codepoint) {
@@ -82,22 +87,93 @@ public class SinFastBlankLibrary implements Library {
82
87
  }
83
88
  }
84
89
 
85
- private static IRubyObject blankUnicodeSlow(
86
- ThreadContext context, byte[] bytes, int s, int e, Encoding enc) {
87
- Ruby runtime = context.runtime;
88
- int[] len = {0};
90
+ /*
91
+ * ActiveSupport's blank regexp matches [[:space:]] with the ctype table of the string's own
92
+ * encoding, which differs from the Unicode table for some encodings (e.g. 0x85 is blank in
93
+ * UTF-8 but not in ISO-8859-1 or ASCII-8BIT). isSpace() reads that same table.
94
+ *
95
+ * Only single-byte codes reach it, and that is not an optimization. Emacs-Mule and the
96
+ * stateless ISO-2022-JP variants answer every ctype query for a multi-byte code with true
97
+ * (EmacsMuleEncoding returns codeToMbcLength(code) > 1 whatever ctype is asked for), and wide
98
+ * GB18030 codepoints arrive negative in a signed int. Falling back to the switch loses
99
+ * nothing: in an ASCII-compatible encoding a multi-byte codepoint always starts at 0x8000 or
100
+ * above, past the U+3000 the switch tops out at, so it only ever answers "not blank" there.
101
+ */
102
+ private static boolean isBlankCodepoint(int codepoint, Encoding enc) {
103
+ if (enc.isUnicode() || codepoint < 0 || codepoint > MAX_CTYPE_CODEPOINT) {
104
+ return isUnicodeBlank(codepoint);
105
+ }
106
+ return enc.isSpace(codepoint);
107
+ }
89
108
 
109
+ /*
110
+ * Handles the rest of an ASCII-compatible string once its first non-ASCII byte is found: ASCII
111
+ * runs stay on the bytewise scan and only non-ASCII characters go through the decoder. The
112
+ * decoder is entered only on character boundaries: every byte the bytewise scan steps over is
113
+ * a single-byte character, and decoded characters are skipped whole.
114
+ *
115
+ * blankUnicodeSlow stays for what this cannot serve: where the encoding is not
116
+ * ASCII-compatible, a byte below 0x80 is not a character on its own, so every character has to
117
+ * go through the decoder.
118
+ */
119
+ private static IRubyObject blankMixed(
120
+ ThreadContext context, RubyString str, byte[] bytes, int s, int e, Encoding enc) {
90
121
  while (s < e) {
91
- int codepoint = EncodingUtils.encCodepointLength(runtime, bytes, s, e, len, enc);
92
- if (!isUnicodeBlank(codepoint)) {
122
+ byte c = bytes[s];
123
+ if (c >= 0) {
124
+ if (!isAsciiBlank(c)) {
125
+ return context.fals;
126
+ }
127
+ s++;
128
+ continue;
129
+ }
130
+ int length = StringSupport.preciseLength(enc, bytes, s, e);
131
+ if (!StringSupport.MBCLEN_CHARFOUND_P(length)) {
132
+ return blankUndecodable(context, str, enc);
133
+ }
134
+ int codepoint = enc.mbcToCode(bytes, s, e);
135
+ if (!isBlankCodepoint(codepoint, enc)) {
93
136
  return context.fals;
94
137
  }
95
- s += len[0];
138
+ s += StringSupport.MBCLEN_CHARFOUND_LEN(length);
96
139
  }
97
140
 
98
141
  return context.tru;
99
142
  }
100
143
 
144
+ private static IRubyObject blankUnicodeSlow(
145
+ ThreadContext context, RubyString str, byte[] bytes, int s, int e, Encoding enc) {
146
+ while (s < e) {
147
+ int length = StringSupport.preciseLength(enc, bytes, s, e);
148
+ if (!StringSupport.MBCLEN_CHARFOUND_P(length)) {
149
+ return blankUndecodable(context, str, enc);
150
+ }
151
+ int codepoint = enc.mbcToCode(bytes, s, e);
152
+ if (!isBlankCodepoint(codepoint, enc)) {
153
+ return context.fals;
154
+ }
155
+ s += StringSupport.MBCLEN_CHARFOUND_LEN(length);
156
+ }
157
+
158
+ return context.tru;
159
+ }
160
+
161
+ /*
162
+ * Reached when the scanner cannot decode the bytes ahead. ActiveSupport's regexp never rescans:
163
+ * it trusts the code range Ruby cached on the string, so it raises only for a string Ruby itself
164
+ * calls broken, and answers "not blank" for one whose transcoder and scanner disagree. Reading
165
+ * the cached code range costs nothing; computing an uncomputed one would scan the whole string,
166
+ * give up the early exit this loop exists for, and only ever come out broken anyway, since it
167
+ * runs the decode that just failed here.
168
+ */
169
+ private static IRubyObject blankUndecodable(
170
+ ThreadContext context, RubyString str, Encoding enc) {
171
+ if (str.getCodeRange() == StringSupport.CR_VALID) {
172
+ return context.fals;
173
+ }
174
+ throw context.runtime.newArgumentError("invalid byte sequence in " + enc);
175
+ }
176
+
101
177
  @JRubyMethod(name = "ascii_blank?")
102
178
  public static IRubyObject ascii_blank_p(ThreadContext context, IRubyObject self) {
103
179
  RubyString str = (RubyString) self;
@@ -111,13 +187,15 @@ public class SinFastBlankLibrary implements Library {
111
187
  int e = s + byteList.realSize();
112
188
  Encoding enc = str.getEncoding();
113
189
 
190
+ /*
191
+ * This one never raises, because bytes that decode to nothing are no more an ASCII blank
192
+ * than the characters they failed to form. An ASCII-compatible encoding does not even need
193
+ * the decoder: a byte of 0x80 or above only ever starts a character whose codepoint is 0x80
194
+ * or above there, so the first one settles the answer.
195
+ */
114
196
  if (enc.isAsciiCompatible()) {
115
197
  for (int i = s; i < e; i++) {
116
- byte c = bytes[i];
117
- if (c < 0) {
118
- return asciiBlankUnicodeSlow(context, bytes, i, e, enc);
119
- }
120
- if (!isAsciiBlankOrNull(c)) {
198
+ if (!isAsciiBlankOrNull(bytes[i])) {
121
199
  return context.fals;
122
200
  }
123
201
  }
@@ -128,24 +206,28 @@ public class SinFastBlankLibrary implements Library {
128
206
  }
129
207
 
130
208
  private static boolean isAsciiBlankOrNull(byte c) {
131
- return c == 0 || (c >= 0x09 && c <= 0x0d) || c == 0x20;
209
+ return c == 0x00
210
+ || (c >= ASCII_WS_RANGE_MIN && c <= ASCII_WS_RANGE_MAX)
211
+ || c == ASCII_WS_SPACE;
132
212
  }
133
213
 
134
214
  private static boolean isAsciiSpace(int codepoint) {
135
- return codepoint == ' ' || ('\t' <= codepoint && codepoint <= '\r');
215
+ return (codepoint >= ASCII_WS_RANGE_MIN && codepoint <= ASCII_WS_RANGE_MAX)
216
+ || codepoint == ASCII_WS_SPACE;
136
217
  }
137
218
 
138
219
  private static IRubyObject asciiBlankUnicodeSlow(
139
220
  ThreadContext context, byte[] bytes, int s, int e, Encoding enc) {
140
- Ruby runtime = context.runtime;
141
- int[] len = {0};
142
-
143
221
  while (s < e) {
144
- int codepoint = EncodingUtils.encCodepointLength(runtime, bytes, s, e, len, enc);
222
+ int length = StringSupport.preciseLength(enc, bytes, s, e);
223
+ if (!StringSupport.MBCLEN_CHARFOUND_P(length)) {
224
+ return context.fals;
225
+ }
226
+ int codepoint = enc.mbcToCode(bytes, s, e);
145
227
  if (codepoint != 0 && !isAsciiSpace(codepoint)) {
146
228
  return context.fals;
147
229
  }
148
- s += len[0];
230
+ s += StringSupport.MBCLEN_CHARFOUND_LEN(length);
149
231
  }
150
232
 
151
233
  return context.tru;
Binary file
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SinFastBlank
4
+ VERSION = '5.0.0'
5
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ # Explicitly undefine method before redefining to avoid Ruby warnings.
5
+ undef_method(:blank?) if method_defined?(:blank?)
6
+ end
7
+
8
+ case RUBY_ENGINE
9
+ when 'jruby'
10
+ require 'jruby'
11
+ require 'sin_fast_blank/sin_fast_blank.jar'
12
+
13
+ Java::sin_fast_blank::SinFastBlankLibrary.new.load(JRuby.runtime, false)
14
+ else
15
+ # The extension-less require lets Ruby resolve the platform-specific shared library suffix (.bundle on macOS, .so elsewhere) via DLEXT.
16
+ require 'sin_fast_blank/sin_fast_blank'
17
+ end
18
+
19
+ require 'sin_fast_blank/version'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sin_fast_blank
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.1
4
+ version: 5.0.0
5
5
  platform: java
6
6
  authors:
7
7
  - Masahiro
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-02-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: Check for blank string faster than FastBlank or ActiveSupport
14
13
  email:
@@ -17,23 +16,25 @@ executables: []
17
16
  extensions: []
18
17
  extra_rdoc_files: []
19
18
  files:
19
+ - CHANGELOG.md
20
+ - LICENSE.txt
21
+ - README.md
22
+ - Rakefile
20
23
  - ext/java/sin_fast_blank/SinFastBlankLibrary.java
24
+ - lib/sin_fast_blank.rb
21
25
  - lib/sin_fast_blank/sin_fast_blank.jar
22
- homepage: https://github.com/cadenza-tech/sin_fast_blank/tree/v4.0.1
26
+ - lib/sin_fast_blank/version.rb
27
+ homepage: https://github.com/cadenza-tech/sin_fast_blank/tree/v5.0.0
23
28
  licenses:
24
29
  - MIT
25
30
  metadata:
26
- homepage_uri: https://github.com/cadenza-tech/sin_fast_blank/tree/v4.0.1
27
- source_code_uri: https://github.com/cadenza-tech/sin_fast_blank/tree/v4.0.1
28
- changelog_uri: https://github.com/cadenza-tech/sin_fast_blank/blob/v4.0.1/CHANGELOG.md
31
+ homepage_uri: https://github.com/cadenza-tech/sin_fast_blank/tree/v5.0.0
32
+ source_code_uri: https://github.com/cadenza-tech/sin_fast_blank/tree/v5.0.0
33
+ changelog_uri: https://github.com/cadenza-tech/sin_fast_blank/blob/v5.0.0/CHANGELOG.md
29
34
  bug_tracker_uri: https://github.com/cadenza-tech/sin_fast_blank/issues
30
- documentation_uri: https://rubydoc.info/gems/sin_fast_blank/4.0.1
31
- funding_uri: https://patreon.com/CadenzaTech
35
+ documentation_uri: https://rubydoc.info/gems/sin_fast_blank/5.0.0
36
+ funding_uri: https://github.com/sponsors/cadenza-tech
32
37
  rubygems_mfa_required: 'true'
33
- required_jruby_version: ">= 9.3.0.0"
34
- required_truffleruby_version: ">= 22.0.0"
35
- required_truffleruby+graalvm_version: ">= 22.0.0"
36
- post_install_message:
37
38
  rdoc_options: []
38
39
  require_paths:
39
40
  - lib
@@ -48,8 +49,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
48
49
  - !ruby/object:Gem::Version
49
50
  version: '0'
50
51
  requirements: []
51
- rubygems_version: 3.2.33
52
- signing_key:
52
+ rubygems_version: 4.0.3
53
53
  specification_version: 4
54
54
  summary: Check for blank string faster than FastBlank or ActiveSupport
55
55
  test_files: []