string_splitter 0.5.1 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 582dd9d8bae0421a49348bf0ccade081a4cc448e8e27943dcb67004b1b684f6d
4
- data.tar.gz: 10990476dec6bf7edc909cd8558d0404fd9295820238ac527ebf3294454815a2
3
+ metadata.gz: d922735ed5c3b8acdc9f0fa0d0c439f0293e1b72739dd1f6b9ef9018a332f6c9
4
+ data.tar.gz: b61f3b6e827675abd5fe1457a000735c4ae4a4a11dc858fc705b783820230fce
5
5
  SHA512:
6
- metadata.gz: 666914aa76ca9f425dc7ef60b0110dbb1239fad3ae44ac49ba0ee59531b93d800cb2ca475c524ee359dbde4b21a0b97a89fa3f6910bb78d1b6737729ffddc1a9
7
- data.tar.gz: 4c9522bcc4e858a98e4b9c79abe2ecf845b0a8209479b802637936215c0a5c02e9c0853f103779618636774ec5ce55a7157ea8144eaadaa97f918a94e062d4e9
6
+ metadata.gz: f226e28ffb81f405ac986c01bf0cdb4270512d0a595d89d7d77ad1b53ed25dd122dac084887de26e26293d214906709724f018b83353a9928209257f6c80bc9e
7
+ data.tar.gz: f56357c60d8d52ff577a1ee5c4d16bbd4082bf29853dc3593fad3eefd6670a345ed245534944f5972fca1909e7e16526351eab898e4832d7cb2054ec86be4851
@@ -1,37 +1,102 @@
1
+ ## 0.7.3 - 2020-08-24
2
+
3
+ #### Changes
4
+
5
+ - avoid exposing an internal Split method inside blocks
6
+
7
+ ## 0.7.2 - 2020-08-22
8
+
9
+ #### Fixes
10
+
11
+ - fix/test default delimiter + `remove_empty_fields`
12
+
13
+ ## 0.7.1 - 2020-08-22
14
+
15
+ #### Changes
16
+
17
+ - performance improvements
18
+ - delegate to `String#split` where possible
19
+ - use a regular class for Split rather than values.rb
20
+ - create Split objects directly rather than allocating intermediate hashes
21
+
22
+ ## 0.7.0 - 2020-08-21
23
+
24
+ #### Breaking Changes
25
+
26
+ - `String#split` incompatibility: we no longer trim the string (with
27
+ `String#strip`) before splitting if the delimiter is omitted
28
+
29
+ ## 0.6.0 - 2020-08-20
30
+
31
+ #### Breaking Changes
32
+
33
+ - `ss.split(str, " ")` is no longer treated the same as `ss.split(str)` i.e.
34
+ unlike Ruby's `String#split`, the former no longer strips the string before
35
+ splitting
36
+ - rename the `remove_empty` option `remove_empty_fields`
37
+ - rename the `exclude` option `except` (alias for `reject`)
38
+
39
+ #### Features
40
+
41
+ - add support for descending, negative, and infinite ranges,
42
+ e.g. `ss.split(str, ":", at: [..4, 4..., 3..1, -1..-3])` etc.
43
+
44
+ #### Fixes
45
+
46
+ - correctly handle backreferences in delimiter patterns
47
+
1
48
  ## 0.5.1 - 2018-07-01
2
49
 
50
+ #### Changes
51
+
3
52
  - set StringSplitter::VERSION when `string_splitter.rb` is loaded
4
- - doc tweaks
5
53
 
6
54
  ## 0.5.0 - 2018-06-26
7
55
 
8
- - don't treat string delimiters as patterns
56
+ #### Features
57
+
9
58
  - add a `reject`/`exclude` option which rejects splits at the specified positions
10
59
  - add a `select` alias for `at`
11
60
 
61
+ #### Fixes
62
+
63
+ - don't treat string delimiters as patterns
64
+
12
65
  ## 0.4.0 - 2018-06-24
13
66
 
14
- - **breaking change**: remove the `offset` alias for `split.index`
67
+ #### Breaking Changes
68
+
69
+ - remove the `offset` alias for `split.index`
15
70
 
16
71
  ## 0.3.1 - 2018-06-24
17
72
 
18
- - remove trailing empty field when the separator is empty ([#1](https://github.com/chocolateboy/string_splitter/issues/1))
73
+ #### Fixes
74
+
75
+ - remove trailing empty field when the separator is empty
76
+ ([#1](https://github.com/chocolateboy/string_splitter/issues/1))
19
77
 
20
78
  ## 0.3.0 - 2018-06-23
21
79
 
22
- - **breaking change**: rename the `default_separator` option to `default_delimiter`
23
- - to avoid ambiguity in the code, refer to the input pattern/string as the
24
- "delimiter" and the matched string as the "separator"
80
+ #### Breaking Changes
81
+
82
+ - rename the `default_separator` option `default_delimiter`
25
83
 
26
84
  ## 0.2.0 - 2018-06-22
27
85
 
28
- - **breaking change**: make `index` (AKA `offset`) 0-based and add `position`
29
- (AKA `pos`) as the 1-based accessor
86
+ #### Breaking Changes
87
+
88
+ - make `index` (AKA `offset`) 0-based and add `position` (AKA `pos`) as the
89
+ 1-based accessor
30
90
 
31
91
  ## 0.1.0 - 2018-06-22
32
92
 
33
- - **breaking change**: the block now takes a single `split` object with an
34
- `index` accessor, rather than seperate `index` and `split` arguments
93
+ #### Breaking Changes
94
+
95
+ - the block now takes a single `split` object with an `index` accessor, rather
96
+ than separate `index` and `split` arguments
97
+
98
+ #### Features
99
+
35
100
  - add support for negative indices in the value supplied to the `at` option
36
101
  - add a `count` field to the split object containing the total number of splits
37
102
 
data/README.md CHANGED
@@ -3,14 +3,15 @@
3
3
  [![Build Status](https://travis-ci.org/chocolateboy/string_splitter.svg)](https://travis-ci.org/chocolateboy/string_splitter)
4
4
  [![Gem Version](https://img.shields.io/gem/v/string_splitter.svg)](https://rubygems.org/gems/string_splitter)
5
5
 
6
- <!-- START doctoc generated TOC please keep comment here to allow auto update -->
7
- <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
6
+ <!-- toc -->
8
7
 
9
8
  - [NAME](#name)
10
9
  - [INSTALLATION](#installation)
11
10
  - [SYNOPSIS](#synopsis)
12
11
  - [DESCRIPTION](#description)
13
12
  - [WHY?](#why)
13
+ - [CAVEATS](#caveats)
14
+ - [Differences from String#split](#differences-from-stringsplit)
14
15
  - [COMPATIBILITY](#compatibility)
15
16
  - [VERSION](#version)
16
17
  - [SEE ALSO](#see-also)
@@ -19,7 +20,7 @@
19
20
  - [AUTHOR](#author)
20
21
  - [COPYRIGHT AND LICENSE](#copyright-and-license)
21
22
 
22
- <!-- END doctoc generated TOC please keep comment here to allow auto update -->
23
+ <!-- tocstop -->
23
24
 
24
25
  # NAME
25
26
 
@@ -42,16 +43,25 @@ ss = StringSplitter.new
42
43
  **Same as `String#split`**
43
44
 
44
45
  ```ruby
45
- ss.split("foo bar baz quux")
46
- ss.split("foo bar baz quux", " ")
47
- ss.split("foo bar baz quux", /\s+/)
48
- # => ["foo", "bar", "baz", "quux"]
46
+ ss.split("foo bar baz")
47
+ ss.split("foo bar baz", " ")
48
+ ss.split("foo bar baz", /\s+/)
49
+ # => ["foo", "bar", "baz"]
50
+
51
+ ss.split("foo", "")
52
+ ss.split("foo", //)
53
+ # => ["f", "o", "o"]
54
+
55
+ ss.split("", "...")
56
+ ss.split("", /.../)
57
+ # => []
49
58
  ```
50
59
 
51
60
  **Split at the first delimiter**
52
61
 
53
62
  ```ruby
54
63
  ss.split("foo:bar:baz:quux", ":", at: 1)
64
+ ss.split("foo:bar:baz:quux", ":", select: 1)
55
65
  # => ["foo", "bar:baz:quux"]
56
66
  ```
57
67
 
@@ -65,54 +75,91 @@ ss.split("foo:bar:baz:quux", ":", at: -1)
65
75
  **Split at multiple delimiter positions**
66
76
 
67
77
  ```ruby
68
- ss.split("1:2:3:4:5:6:7:8:9", ":", at: [1..3, -2])
69
- # => ["1", "2", "3", "4:5:6:7", "8:9"]
78
+ ss.split("1:2:3:4:5:6:7:8:9", ":", at: [1..3, -1])
79
+ # => ["1", "2", "3", "4:5:6:7:8", "9"]
80
+ ```
81
+
82
+ **Split at all but the first and last delimiters**
83
+
84
+ ```ruby
85
+ ss.split("1:2:3:4:5:6", ":", except: [1, -1])
86
+ ss.split("1:2:3:4:5:6", ":", reject: [1, -1])
87
+ # => ["1:2", "3", "4", "5:6"]
70
88
  ```
71
89
 
72
90
  **Split from the right**
73
91
 
74
92
  ```ruby
75
- ss.rsplit("1:2:3:4:5:6:7:8:9", ":", at: [1..3, 5])
76
- # => ["1:2:3:4", "5:6", "7", "8", "9"]
93
+ ss.rsplit("1:2:3:4:5:6:7:8:9", ":", at: [1..3, -1])
94
+ # => ["1", "2:3:4:5:6", "7", "8", "9"]
95
+ ```
96
+
97
+ **Split with negative, descending, and infinite ranges**
98
+
99
+ ```ruby
100
+ ss.split("1:2:3:4:5:6:7:8:9", ":", at: ..-3)
101
+ # => ["1", "2", "3", "4", "5", "6", "7:8:9"]
102
+
103
+ ss.split("1:2:3:4:5:6:7:8:9", ":", at: 4...)
104
+ # => ["1:2:3:4", "5", "6", "7", "8:9"]
105
+
106
+ ss.split("1:2:3:4:5:6:7:8:9", ":", at: [1, 5..3, -2..])
107
+ # => ["1", "2:3", "4", "5", "6:7", "8", "9"]
77
108
  ```
109
+
78
110
  **Full control via a block**
79
111
 
80
112
  ```ruby
81
- result = ss.split('a:a:a:b:c:c:e:a:a:d:c', ":") do |split|
82
- split.index > 0 && split.lhs == split.rhs
113
+ result = ss.split("1:2:3:4:5:6:7:8", ":") do |split|
114
+ split.pos % 2 == 0
83
115
  end
84
- # => ["a:a", "a:b:c", "c:e:a", "a:d:c"]
116
+ # => ["1:2", "3:4", "5:6", "7:8"]
117
+ ```
118
+
119
+ ```ruby
120
+ string = "banana".chars.sort.join # "aaabnn"
121
+
122
+ ss.split(string, "") do |split|
123
+ split.rhs != split.lhs
124
+ end
125
+ # => ["aaa", "b", "nn"]
85
126
  ```
86
127
 
87
128
  # DESCRIPTION
88
129
 
89
- Many languages have built-in `split` functions/methods for strings. They behave similarly
90
- (notwithstanding the occasional [surprise](https://chriszetter.com/blog/2017/10/29/splitting-strings/)),
91
- and handle a few common cases e.g.:
130
+ Many languages have built-in `split` functions/methods for strings. They behave
131
+ similarly (notwithstanding the occasional
132
+ [surprise](https://chriszetter.com/blog/2017/10/29/splitting-strings/)), and
133
+ handle a few common cases, e.g.:
92
134
 
93
135
  * limiting the number of splits
94
136
  * including the separator(s) in the results
95
137
  * removing (some) empty fields
96
138
 
97
- But, because the API is squeezed into two overloaded parameters (the delimiter and the limit),
98
- achieving the desired results can be tricky. For instance, while `String#split` removes empty
99
- trailing fields (by default), it provides no way to remove *all* empty fields. Likewise, the
100
- cramped API means there's no way to e.g. combine a limit (positive integer) with the option
101
- to preserve empty fields (negative integer), or use backreferences in a delimiter pattern
139
+ But, because the API is squeezed into two overloaded parameters (the delimiter
140
+ and the limit), achieving the desired results can be tricky. For instance,
141
+ while `String#split` removes empty trailing fields (by default), it provides no
142
+ way to remove *all* empty fields. Likewise, the cramped API means there's no
143
+ way to, e.g., combine a limit (positive integer) with the option to preserve
144
+ empty fields (negative integer), or use backreferences in a delimiter pattern
102
145
  without including its captured subexpressions in the result.
103
146
 
104
- If `split` was being written from scratch, without the baggage of its legacy API,
105
- it's possible that some of these options would be made explicit rather than overloading
106
- the parameters. And, indeed, this is possible in some implementations,
107
- e.g. in Crystal:
147
+ If `split` was being written from scratch, without the baggage of its legacy
148
+ API, it's possible that some of these options would be made explicit rather
149
+ than overloading the parameters. And, indeed, this is possible in some
150
+ implementations, e.g. in Crystal:
108
151
 
109
152
  ```ruby
110
- ":foo:bar:baz:".split(":", remove_empty: false) # => ["", "foo", "bar", "baz", ""]
111
- ":foo:bar:baz:".split(":", remove_empty: true) # => ["foo", "bar", "baz"]
153
+ ":foo:bar:baz:".split(":", remove_empty: false)
154
+ # => ["", "foo", "bar", "baz", ""]
155
+
156
+ ":foo:bar:baz:".split(":", remove_empty: true)
157
+ # => ["foo", "bar", "baz"]
112
158
  ````
113
159
 
114
- StringSplitter takes this one step further by moving the configuration out of the method altogether
115
- and delegating the strategy — i.e. which splits should be accepted or rejected — to a block:
160
+ StringSplitter takes this one step further by moving the configuration out of
161
+ the method altogether and delegating the strategy — i.e. which splits should be
162
+ accepted or rejected — to a block:
116
163
 
117
164
  ```ruby
118
165
  ss = StringSplitter.new
@@ -120,23 +167,32 @@ ss = StringSplitter.new
120
167
  ss.split("foo:bar:baz", ":") { |split| split.index == 0 }
121
168
  # => ["foo", "bar:baz"]
122
169
 
123
- ss.split("foo:bar:baz", ":") { |split| split.position == split.count }
124
- # => ["foo:bar", "baz"]
170
+ ss.split("foo:bar:baz:quux", ":") do |split|
171
+ split.position == 1 || split.position == 3
172
+ end
173
+ # => ["foo", "bar:baz", "quux"]
125
174
  ```
126
175
 
127
- As a shortcut, the common case of splitting on delimiters at one or more positions is supported by an option:
176
+ As a shortcut, the common case of splitting (or not splitting) at one or more
177
+ positions is supported by dedicated options:
128
178
 
129
179
  ```ruby
130
- ss.split('foo:bar:baz:quux', ':', at: [1, -1]) # => ["foo", "bar:baz", "quux"]
180
+ ss.split("foo:bar:baz:quux", ":", select: [1, -1])
181
+ # => ["foo", "bar:baz", "quux"]
182
+
183
+ ss.split("foo:bar:baz:quux", ":", reject: [1, -1])
184
+ # => ["foo:bar", "baz:quux"]
131
185
  ```
132
186
 
133
187
  # WHY?
134
188
 
135
- I wanted to split semi-structured output into fields without having to resort to a regex or a full-blown parser.
189
+ I wanted to split semi-structured output into fields without having to resort
190
+ to a regex or a full-blown parser.
136
191
 
137
- As an example, the nominally unstructured output of many Unix commands is often formatted in a way
138
- that's tantalizingly close to being [machine-readable](https://en.wikipedia.org/wiki/Delimiter-separated_values),
139
- apart from a few pesky exceptions e.g.:
192
+ As an example, the nominally unstructured output of many Unix commands is often
193
+ formatted in a way that's tantalizingly close to being
194
+ [machine-readable](https://en.wikipedia.org/wiki/Delimiter-separated_values),
195
+ apart from a few pesky exceptions, e.g.:
140
196
 
141
197
  ```bash
142
198
  $ ls -l
@@ -148,8 +204,8 @@ drwxr-xr-x 3 user users 4096 Jun 19 22:56 lib
148
204
  -rw-r--r-- 1 user users 3134 Jun 19 22:59 README.md
149
205
  ```
150
206
 
151
- These lines can *almost* be parsed into an array of fields by splitting them on whitespace. The exception is the
152
- date (columns 6-8) i.e.:
207
+ These lines can *almost* be parsed into an array of fields by splitting them on
208
+ whitespace. The exception is the date (columns 6-8), i.e.:
153
209
 
154
210
  ```ruby
155
211
  line = "-rw-r--r-- 1 user users 87 Jun 18 18:16 CHANGELOG.md"
@@ -168,19 +224,20 @@ instead of:
168
224
  ["-rw-r--r--", "1", "user", "users", "87", "Jun 18 18:16", "CHANGELOG.md"]
169
225
  ```
170
226
 
171
- One way to work around this is to parse the whole line e.g.:
227
+ One way to work around this is to parse the whole line, e.g.:
172
228
 
173
229
  ```ruby
174
230
  line.match(/^(\S+) \s+ (\d+) \s+ (\S+) \s+ (\S+) \s+ (\d+) \s+ (\S+ \s+ \d+ \s+ \S+) \s+ (.+)$/x)
175
231
  ```
176
232
 
177
- But that requires us to specify *everything*. What we really want is a version of `split`
178
- which allows us to veto splitting for the 6th and 7th delimiters i.e. control over which
179
- splits are accepted, rather than being restricted to the single, baked-in strategy provided
180
- by the `limit` parameter.
233
+ But that requires us to specify *everything*. What we really want is a version
234
+ of `split` which allows us to veto splitting for the 6th and 7th delimiters
235
+ (and to stop after the 8th delimiter), i.e. control over which splits are
236
+ accepted, rather than being restricted to the single, baked-in strategy
237
+ provided by the `limit` parameter.
181
238
 
182
- By providing a simple way to accept or reject each split, StringSplitter makes cases like
183
- this easy to handle, either via a block:
239
+ By providing a simple way to accept or reject each split, StringSplitter makes
240
+ cases like this easy to handle, either via a block:
184
241
 
185
242
  ```ruby
186
243
  ss.split(line) do |split|
@@ -196,14 +253,51 @@ ss.split(line, at: [1..5, 8])
196
253
  # => ["-rw-r--r--", "1", "user", "users", "87", "Jun 18 18:16", "CHANGELOG.md"]
197
254
  ```
198
255
 
256
+ # CAVEATS
257
+
258
+ ## Differences from String#split
259
+
260
+ Unlike `String#split`, StringSplitter doesn't trim the string before splitting
261
+ if the delimiter is omitted or a single space, e.g.:
262
+
263
+ ```ruby
264
+ " foo bar baz ".split # => ["foo", "bar", "baz"]
265
+ " foo bar baz ".split(" ") # => ["foo", "bar", "baz"]
266
+
267
+ ss.split(" foo bar baz ") # => ["", "foo", "bar", "baz", ""]
268
+ ss.split(" foo bar baz ", " ") # => ["", "foo", "bar", "baz", ""]
269
+ ```
270
+
271
+ `String#split` omits the `nil` values of unmatched optional captures:
272
+
273
+ ```ruby
274
+ "foo:bar:baz".scan(/(:)|(-)/) # => [[":", nil], [":", nil]]
275
+ "foo:bar:baz".split(/(:)|(-)/) # => ["foo", ":", "bar", ":", "baz"]
276
+ ```
277
+
278
+ StringSplitter preserves them by default (if `include_captures` is true, as it
279
+ is by default), though they can be omitted from spread captures by passing
280
+ `:compact` as the value of the `spread_captures` option:
281
+
282
+ ```ruby
283
+ s1 = StringSplitter.new(spread_captures: true)
284
+ s2 = StringSplitter.new(spread_captures: false)
285
+ s3 = StringSplitter.new(spread_captures: :compact)
286
+
287
+ s1.split("foo:bar:baz", /(:)|(-)/) # => ["foo", ":", nil, "bar", ":", nil, "baz"]
288
+ s2.split("foo:bar:baz", /(:)|(-)/) # => ["foo", [":", nil], "bar", [":", nil], "baz"]
289
+ s3.split("foo:bar:baz", /(:)|(-)/) # => ["foo", ":", "bar", ":", "baz"]
290
+ ```
291
+
199
292
  # COMPATIBILITY
200
293
 
201
- StringSplitter is tested and supported on all versions of Ruby [supported by the ruby-core team](https://www.ruby-lang.org/en/downloads/branches/),
202
- i.e., currently, Ruby 2.3 and above.
294
+ StringSplitter is tested and supported on all versions of Ruby [supported by
295
+ the ruby-core team](https://www.ruby-lang.org/en/downloads/branches/), i.e.,
296
+ currently, Ruby 2.5 and above.
203
297
 
204
298
  # VERSION
205
299
 
206
- 0.5.1
300
+ 0.7.3
207
301
 
208
302
  # SEE ALSO
209
303
 
@@ -221,8 +315,7 @@ i.e., currently, Ruby 2.3 and above.
221
315
 
222
316
  # COPYRIGHT AND LICENSE
223
317
 
224
- Copyright © 2018 by chocolateboy.
318
+ Copyright © 2018-2020 by chocolateboy.
225
319
 
226
320
  This is free software; you can redistribute it and/or modify it under the
227
- terms of the [Artistic License 2.0](http://www.opensource.org/licenses/artistic-license-2.0.php).
228
-
321
+ terms of the [Artistic License 2.0](https://www.opensource.org/licenses/artistic-license-2.0.php).
@@ -1,54 +1,94 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'values'
3
+ require 'set'
4
+
5
+ require_relative 'string_splitter/split'
4
6
  require_relative 'string_splitter/version'
5
7
 
6
8
  # This class extends the functionality of +String#split+ by:
7
9
  #
8
10
  # - providing full control over which splits are accepted or rejected
11
+ #
9
12
  # - adding support for splitting from right-to-left
13
+ #
10
14
  # - encapsulating splitting options/preferences in the splitter rather
11
15
  # than trying to cram them into overloaded method parameters
12
16
  #
13
17
  # These enhancements allow splits to handle many cases that otherwise require bigger
14
- # guns e.g. regex matching or parsing.
18
+ # guns, e.g. regex matching or parsing.
19
+ #
20
+ # Implementation-wise, we split the string either with String#split, or with a custom
21
+ # scanner if the delimiter may contain captures (since String#split doesn't handle
22
+ # them correctly), and parse the resulting tokens into an array of Split objects with
23
+ # the following attributes:
24
+ #
25
+ # - captures: separator substrings captured by parentheses in the delimiter pattern
26
+ # - count: the number of splits
27
+ # - index: the 0-based index of the split in the array
28
+ # - lhs: the string to the left of the separator (back to the previous split candidate)
29
+ # - position: the 1-based index of the split in the array (alias: pos)
30
+ # - rhs: the string to the right of the separator (up to the next split candidate)
31
+ # - rindex: the 0-based index of the split relative to the end of the array
32
+ # - rposition: the 1-based index of the split relative to the end of the array (alias: rpos)
33
+ # - separator: the string matched by the delimiter pattern/string
34
+ #
15
35
  class StringSplitter
16
- ACCEPT_ALL = ->(_split) { true }
17
- DEFAULT_DELIMITER = /\s+/
18
- NO_SPLITS = []
36
+ # terminology: the delimiter is what we provide and the separators are what we get
37
+ # back (if we capture them). e.g. for:
38
+ #
39
+ # ss.split("foo:bar::baz", /(\W+)/)
40
+ #
41
+ # the delimiter is /(\W)/ and the separators are ":" and "::"
19
42
 
20
- Split = Value.new(:captures, :count, :index, :lhs, :rhs, :separator) do
21
- def position
22
- index + 1
23
- end
43
+ # pull in the StringSplitter::Split#update! method
44
+ using Split::Refinements
24
45
 
25
- alias_method :pos, :position
46
+ ACCEPT_ALL = ->(_split) { true }
47
+ DEFAULT_DELIMITER = /\s+/.freeze
48
+ REMOVE = [].freeze
49
+
50
+ # simulate an enum. the value is returned by the case statement
51
+ # in the generated block if the positions match
52
+ module Action
53
+ SELECT = true
54
+ REJECT = false
26
55
  end
27
56
 
57
+ private_constant :Action
58
+
28
59
  def initialize(
29
60
  default_delimiter: DEFAULT_DELIMITER,
30
61
  include_captures: true,
31
- remove_empty: false,
62
+ remove_empty: false, # TODO remove this
63
+ remove_empty_fields: remove_empty,
32
64
  spread_captures: true
33
65
  )
34
66
  @default_delimiter = default_delimiter
35
67
  @include_captures = include_captures
36
- @remove_empty = remove_empty
68
+ @remove_empty_fields = remove_empty_fields
37
69
  @spread_captures = spread_captures
38
70
  end
39
71
 
40
- attr_reader :default_delimiter, :include_captures, :remove_empty, :spread_captures
72
+ attr_reader(
73
+ :default_delimiter,
74
+ :include_captures,
75
+ :remove_empty_fields,
76
+ :spread_captures
77
+ )
78
+
79
+ # TODO remove this
80
+ alias remove_empty remove_empty_fields
41
81
 
42
82
  def split(
43
83
  string,
44
84
  delimiter = @default_delimiter,
45
- at: nil,
85
+ at: nil, # alias for select
86
+ except: nil, # alias for reject
46
87
  select: at,
47
- exclude: nil,
48
- reject: exclude,
88
+ reject: except,
49
89
  &block
50
90
  )
51
- result, splits, block = split_init(
91
+ result, splits, count, accept = init(
52
92
  string: string,
53
93
  delimiter: delimiter,
54
94
  select: select,
@@ -56,29 +96,22 @@ class StringSplitter
56
96
  block: block
57
97
  )
58
98
 
59
- count = splits.length
99
+ return result unless splits
100
+
101
+ result << splits.first.lhs
60
102
 
61
103
  splits.each_with_index do |split, index|
62
- split = Split.with(split.merge({ index: index, count: count }))
63
- result << split.lhs if result.empty?
64
-
65
- if block.call(split)
66
- if @include_captures
67
- if @spread_captures
68
- result += split.captures
69
- else
70
- result << split.captures
71
- end
72
- end
104
+ split.update!(count: count, index: index)
73
105
 
74
- result << split.rhs
106
+ if accept.call(split)
107
+ result << split.captures << split.rhs
75
108
  else
76
- # concatenate the rhs
109
+ # append the rhs
77
110
  result[-1] = result[-1] + split.separator + split.rhs
78
111
  end
79
112
  end
80
113
 
81
- result
114
+ render(result)
82
115
  end
83
116
 
84
117
  alias lsplit split
@@ -86,13 +119,13 @@ class StringSplitter
86
119
  def rsplit(
87
120
  string,
88
121
  delimiter = @default_delimiter,
89
- at: nil,
122
+ at: nil, # alias for select
123
+ except: nil, # alias for reject
90
124
  select: at,
91
- exclude: nil,
92
- reject: exclude,
125
+ reject: except,
93
126
  &block
94
127
  )
95
- result, splits, block = split_init(
128
+ result, splits, count, accept = init(
96
129
  string: string,
97
130
  delimiter: delimiter,
98
131
  select: select,
@@ -100,203 +133,262 @@ class StringSplitter
100
133
  block: block
101
134
  )
102
135
 
103
- count = splits.length
136
+ return result unless splits
104
137
 
105
- splits.reverse!.each_with_index do |split, index|
106
- split = Split.with(split.merge({ index: index, count: count }))
107
- result.unshift(split.rhs) if result.empty?
108
-
109
- if block.call(split)
110
- if @include_captures
111
- if @spread_captures
112
- result = split.captures + result
113
- else
114
- result.unshift(split.captures)
115
- end
116
- end
138
+ result.unshift(splits.last.rhs)
139
+
140
+ splits.reverse_each.with_index do |split, index|
141
+ split.update!(count: count, index: index)
117
142
 
118
- result.unshift(split.lhs)
143
+ if accept.call(split)
144
+ # [lhs + captures] + result
145
+ result.unshift(split.lhs, split.captures)
119
146
  else
120
147
  # prepend the lhs
121
148
  result[0] = split.lhs + split.separator + result[0]
122
149
  end
123
150
  end
124
151
 
125
- result
152
+ render(result)
126
153
  end
127
154
 
128
155
  private
129
156
 
130
- def splits_for(parts, ncaptures)
131
- result = []
132
- splits = []
157
+ # initialisation common to +split+ and +rsplit+
158
+ #
159
+ # takes a hash of options passed to +split+ or +rsplit+ and returns a tuple with
160
+ # the following fields:
161
+ #
162
+ # - result: the array of separated strings to return from +split+ or +rsplit+.
163
+ # if the splits array is empty, the caller returns this array immediately
164
+ # without any further processing
165
+ #
166
+ # - splits: an array of Split objects exposing the lhs, rhs, separator and
167
+ # captured separator substrings for each split
168
+ #
169
+ # - count: the number of splits
170
+ #
171
+ # - accept: a proc whose return value determines whether each split should be
172
+ # accepted (true) or rejected (false)
173
+ #
174
+ def init(string:, delimiter:, select:, reject:, block:)
175
+ return [[]] if string.empty?
176
+
177
+ unless block
178
+ if reject
179
+ positions = reject
180
+ action = Action::REJECT
181
+ elsif select
182
+ positions = select
183
+ action = Action::SELECT
184
+ else
185
+ block = ACCEPT_ALL
186
+ end
187
+ end
133
188
 
134
- until parts.empty?
135
- lhs = parts.shift
136
- separator = parts.shift
137
- captures = parts.shift(ncaptures)
138
- rhs = parts.length == 1 ? parts.shift : parts.first
139
-
140
- if @remove_empty && (lhs.empty? || rhs.empty?)
141
- if lhs.empty? && rhs.empty?
142
- # do nothing
143
- elsif parts.empty? # last split
144
- result << (!lhs.empty? ? lhs : rhs) if splits.empty?
145
- elsif rhs.empty?
146
- # replace the empty rhs with the non-empty lhs
147
- parts[0] = lhs
148
- end
189
+ # use String#split if we can
190
+ #
191
+ # NOTE +reject!+ is no faster than +reject+ on MRI and significantly slower
192
+ # on TruffleRuby
193
+
194
+ if delimiter.is_a?(String)
195
+ limit = -1
149
196
 
150
- next
197
+ if delimiter == ' '
198
+ delimiter = / / # don't trim
199
+ elsif delimiter.empty?
200
+ limit = 0 # remove the trailing empty string
151
201
  end
152
202
 
153
- splits << {
154
- lhs: lhs,
155
- rhs: rhs,
156
- separator: separator,
157
- captures: captures,
158
- }
159
- end
203
+ result = string.split(delimiter, limit)
160
204
 
161
- [result, splits]
162
- end
205
+ return [result] if result.length == 1 # delimiter not found: no splits
163
206
 
164
- # takes a hash of options passed to +split+ or +rsplit+ and returns a:
165
- #
166
- # [result, splits, block]
167
- #
168
- # triple, where `result` is the return value of the method, `splits` is an array
169
- # of hashes containing the lhs/rhs, separator and captures of each split, and
170
- # `block` is a proc which specifies whether each split should be accepted or
171
- # rejected
172
- def split_init(string:, delimiter:, select:, reject:, block:)
173
- unless (match = string.match(delimiter))
174
- result = (@remove_empty && string.empty?) ? [] : [string]
175
- return [result, NO_SPLITS, block]
176
- end
207
+ if block == ACCEPT_ALL # return the (2 or more) fields
208
+ result = result.reject(&:empty?) if @remove_empty_fields
209
+ return [result]
210
+ end
177
211
 
178
- select = Array(select)
179
- reject = Array(reject)
212
+ splits = []
180
213
 
181
- if !reject.empty?
182
- positions = reject
183
- action = :reject
184
- elsif !select.empty?
185
- positions = select
186
- action = :select
214
+ result.each_cons(2) do |lhs, rhs| # 2 or more fields
215
+ splits << Split.new(
216
+ captures: [],
217
+ lhs: lhs,
218
+ rhs: rhs,
219
+ separator: delimiter
220
+ )
221
+ end
222
+ elsif delimiter == DEFAULT_DELIMITER && block == ACCEPT_ALL
223
+ # non-empty separators so -1 is safe
224
+
225
+ # XXX String#split with block was introduced in Ruby 2.6:
226
+ #
227
+ # - https://rubyreferences.github.io/rubychanges/2.6.html#stringsplit-with-block
228
+ #
229
+ # rather than sniffing, we'll just use the compatible version for now
230
+ #
231
+ # if @remove_empty_fields
232
+ # result = []
233
+ #
234
+ # string.split(delimiter, -1) do |field|
235
+ # result << field unless field.empty?
236
+ # end
237
+ # else
238
+ # result = string.split(delimiter, -1)
239
+ # end
240
+
241
+ result = string.split(delimiter, -1)
242
+ result = result.reject(&:empty?) if @remove_empty_fields
243
+ return [result]
244
+ else
245
+ splits = parse(string, delimiter)
187
246
  end
188
247
 
189
- ncaptures = match.captures.length
190
- delimiter = Regexp.quote(delimiter) if delimiter.is_a?(String)
191
- delimiter = increment_backrefs(delimiter, ncaptures)
192
- parts = string.split(/(#{delimiter})/, -1)
193
- remove_trailing_empty_field!(parts, ncaptures)
194
- result, splits = splits_for(parts, ncaptures)
195
- block ||= positions ? match_positions(positions, action, splits.length) : ACCEPT_ALL
248
+ count = splits.length
196
249
 
197
- [result, splits, block]
250
+ return [[string]] if count.zero?
251
+
252
+ block ||= compile(positions, action, count)
253
+ [[], splits, count, block]
198
254
  end
199
255
 
200
- # increment back-references so they remain valid when the outer capture
201
- # is added.
202
- #
203
- # e.g. to split on:
204
- #
205
- # - <foo-comment> ... </foo-comment>
206
- # - <bar-comment> ... </bar-comment>
207
- #
208
- # etc.
256
+ def render(values)
257
+ values.flat_map do |value|
258
+ if value.is_a?(String)
259
+ value.empty? && @remove_empty_fields ? REMOVE : [value]
260
+ elsif @include_captures
261
+ if @spread_captures
262
+ # TODO make sure compact can return a Capture
263
+ @spread_captures == :compact ? value.compact : value
264
+ elsif value.empty?
265
+ # we expose non-captures (string delimiters or regexps with no
266
+ # captures) as empty arrays inside the block, so the type is
267
+ # consistent, but it doesn't make sense to keep them in the
268
+ # result
269
+ REMOVE
270
+ else
271
+ [value]
272
+ end
273
+ else
274
+ REMOVE
275
+ end
276
+ end
277
+ end
278
+
279
+ # takes a string and a delimiter pattern (regex or string) and splits it along
280
+ # the delimiter, returning an array of objects representing each split.
281
+ # e.g. for:
209
282
  #
210
- # before:
283
+ # parse("foo:bar:baz:quux", ":")
211
284
  #
212
- # %r| <(\w+-comment)> [^<]* </\1-comment> |x
285
+ # we return:
213
286
  #
214
- # after:
287
+ # [
288
+ # Split.new(lhs: "foo", rhs: "bar", separator: ":", captures: []),
289
+ # Split.new(lhs: "bar", rhs: "baz", separator: ":", captures: []),
290
+ # Split.new(lhs: "baz", rhs: "quux", separator: ":", captures: []),
291
+ # ]
215
292
  #
216
- # %r| ( <(\w+-comment)> [^<]* </\2-comment> ) |x
293
+ def parse(string, delimiter)
294
+ # has_names = delimiter.is_a?(Regexp) && !delimiter.names.empty?
295
+ splits = []
296
+ start = 0
217
297
 
218
- def increment_backrefs(delimiter, ncaptures)
219
- if delimiter.is_a?(Regexp) && ncaptures > 0
220
- delimiter = delimiter.to_s.gsub(/\\(?:(\d+)|.)/) do
221
- match = Regexp.last_match
222
- match[1] ? '\\' + match[1].to_i.next.to_s : match[0]
223
- end
298
+ # we don't use the argument passed to the +scan+ block here because it's a
299
+ # string (the separator) if there are no captures, rather than an empty
300
+ # array. we use match.captures instead to get the array
301
+ string.scan(delimiter) do
302
+ match = Regexp.last_match
303
+ index, after = match.offset(0)
304
+ separator = match[0]
305
+
306
+ # ignore empty separators at the beginning and/or end of the string
307
+ next if separator.empty? && (index.zero? || after == string.length)
308
+
309
+ lhs = string.slice(start, index - start)
310
+ splits.last.rhs = lhs unless splits.empty?
311
+
312
+ # this is correct for the last/only match, but gets updated to the next
313
+ # match's lhs for other matches
314
+ rhs = match.post_match
315
+
316
+ # captures = has_names ? Captures.new(match) : match.captures
317
+
318
+ splits << Split.new(
319
+ captures: match.captures,
320
+ lhs: lhs,
321
+ rhs: rhs,
322
+ separator: separator
323
+ )
324
+
325
+ # advance the start index (the start of the next lhs) to the position
326
+ # after the last character of the separator
327
+ start = after
224
328
  end
225
329
 
226
- delimiter
330
+ splits
227
331
  end
228
332
 
229
- # work around Ruby's (and Perl's and Groovy's) unhelpful behavior when splitting
230
- # on an empty string/pattern without removing trailing empty fields e.g.:
333
+ # returns a lambda which splits at (i.e. accepts or rejects splits at, depending
334
+ # on the action) the supplied positions
231
335
  #
232
- # "foobar".split("", -1)
233
- # "foobar".split(//, -1)
234
- # # => ["f", "o", "o", "b", "a", "r", ""]
336
+ # positions are preprocessed to support negative indices, infinite ranges, and
337
+ # descending ranges, e.g.:
235
338
  #
236
- # "foobar".split(/()/, -1)
237
- # # => ["f", "", "o", "", "o", "", "b", "", "a", "", "r", "", ""]
339
+ # ss.split("foo:bar:baz:quux", ":", at: -1)
238
340
  #
239
- # "foobar".split(/(())/, -1)
240
- # # => ["f", "", "", "o", "", "", "o", "", "", "b", "", "", "a", "", "", "r", "", "", ""]
341
+ # translates to:
241
342
  #
242
- # *there is no such thing as an empty field whose separator is empty*, so
243
- # if String#split's result ends with an empty separator, 0 or more (empty)
244
- # captures and an empty field, we can safely remove them.
245
-
246
- def remove_trailing_empty_field!(parts, ncaptures)
247
- # the trailing field is at index -1. if there are 0 captures, the separator
248
- # is at -2:
249
- #
250
- # [empty_separator, empty_field]
251
- #
252
- # if there is 1 capture, the separator is at -3:
253
- #
254
- # [empty_separator, capture, empty_field]
343
+ # ss.split("foo:bar:baz:quux", ":", at: 3)
344
+ #
345
+ # and
346
+ #
347
+ # ss.split("1:2:3:4:5:6:7:8:9", ":", at: -3..)
348
+ #
349
+ # translates to:
350
+ #
351
+ # ss.split("1:2:3:4:5:6:7:8:9", ":", at: 6..8)
352
+ #
353
+ def compile(positions, action, count)
354
+ # XXX note: we don't use modulo, because we don't want
355
+ # out-of-bounds indices to silently work, e.g. we don't want:
255
356
  #
256
- # etc. therefore we find the separator by walking back
357
+ # ss.split("foo:bar:baz:quux", ":", at: -42)
257
358
  #
258
- # 1 (empty field)
259
- # + ncaptures
260
- # + 1 (separator)
359
+ # to mysteriously match when the index/position is 0/1
261
360
  #
262
- # steps from the end of the array i.e. ncaptures + 2
263
- count = ncaptures + 2
264
- separator_index = count * -1
265
-
266
- return unless parts[-1].empty? && parts[separator_index].empty?
267
-
268
- # drop the empty separator, the (empty) captures, and the trailing empty field
269
- parts.pop(count)
270
- end
271
-
272
- def match_positions(positions, action, nsplits)
273
- positions = Array(positions).map do |position|
274
- if position.is_a?(Integer) && position.negative?
275
- # translate negative indices to 1-based non-negative indices e.g:
276
- #
277
- # ss.split("foo:bar:baz:quux", ":", at: -1)
278
- #
279
- # translates to:
280
- #
281
- # ss.split("foo:bar:baz:quux", ":", at: 3)
282
- #
283
- # XXX note: we don't use modulo, because we don't want
284
- # out-of-bounds indices to silently work e.g. we don't want:
285
- #
286
- # ss.split("foo:bar:baz:quux", ":", -42)
287
- #
288
- # to mysteriously match when the position is 2
289
-
290
- nsplits + 1 + position
361
+ resolve = ->(int) { int.negative? ? count + 1 + int : int }
362
+
363
+ # don't use Array(...) to wrap these as we don't want to convert ranges
364
+ positions = positions.is_a?(Array) ? positions : [positions]
365
+
366
+ positions = positions.map do |position|
367
+ if position.is_a?(Integer)
368
+ resolve[position]
369
+ elsif position.is_a?(Range)
370
+ rbegin = position.begin
371
+ rend = position.end
372
+ rexc = position.exclude_end?
373
+
374
+ if rbegin.nil?
375
+ Range.new(1, resolve[rend], rexc)
376
+ elsif rend.nil?
377
+ Range.new(resolve[rbegin], count, rexc)
378
+ elsif rbegin.negative? || rend.negative? || (rend - rbegin).negative?
379
+ from = resolve[rbegin]
380
+ to = resolve[rend]
381
+ to < from ? Range.new(to, from, rexc) : Range.new(from, to, rexc)
382
+ else
383
+ position
384
+ end
385
+ elsif position.is_a?(Set)
386
+ position.map { |it| resolve[it] }.to_set
291
387
  else
292
388
  position
293
389
  end
294
390
  end
295
391
 
296
- match = action == :select
297
-
298
- lambda do |split|
299
- case split.position when *positions then match else !match end
300
- end
392
+ ->(split) { case split.position when *positions then action else !action end }
301
393
  end
302
394
  end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class StringSplitter
4
+ class Split
5
+ # expose the +update!+ method as a refinement to StringSplitter but don't
6
+ # expose it to blocks
7
+ #
8
+ # idea based on a suggestion here (as an alternative to a `friend` modifier):
9
+ # https://bugs.ruby-lang.org/issues/12962#note-5
10
+ module Refinements
11
+ refine Split do
12
+ def update!(count:, index:)
13
+ @count = count
14
+ @index = index
15
+ @position = index + 1
16
+ freeze
17
+ end
18
+ end
19
+ end
20
+
21
+ attr_reader :captures, :count, :index, :lhs, :position, :rhs, :separator
22
+ attr_writer :rhs
23
+
24
+ alias pos position
25
+
26
+ def initialize(captures:, lhs:, rhs:, separator:)
27
+ @captures = captures
28
+ @lhs = lhs
29
+ @rhs = rhs
30
+ @separator = separator
31
+ end
32
+
33
+ # 0-based index relative to the end of the array, e.g. for 5 items:
34
+ #
35
+ # index | rindex
36
+ # ------|-------
37
+ # 0 | 4
38
+ # 1 | 3
39
+ # 2 | 2
40
+ # 3 | 1
41
+ # 4 | 0
42
+ def rindex
43
+ @count - @position
44
+ end
45
+
46
+ # 1-based position relative to the end of the array, e.g. for 5 items:
47
+ #
48
+ # position | rposition
49
+ # ----------|----------
50
+ # 1 | 5
51
+ # 2 | 4
52
+ # 3 | 3
53
+ # 4 | 2
54
+ # 5 | 1
55
+ def rposition
56
+ @count + 1 - @position
57
+ end
58
+
59
+ alias rpos rposition
60
+ end
61
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class StringSplitter
4
- VERSION = '0.5.1'
4
+ VERSION = '0.7.3'
5
5
  end
metadata CHANGED
@@ -1,71 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: string_splitter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - chocolateboy
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-01 00:00:00.000000000 Z
11
+ date: 2020-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: values
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.8'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '1.8'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - "~>"
32
18
  - !ruby/object:Gem::Version
33
- version: '1.16'
19
+ version: '2.1'
34
20
  type: :development
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - "~>"
39
25
  - !ruby/object:Gem::Version
40
- version: '1.16'
26
+ version: '2.1'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: minitest
43
29
  requirement: !ruby/object:Gem::Requirement
44
30
  requirements:
45
31
  - - "~>"
46
32
  - !ruby/object:Gem::Version
47
- version: '5.11'
33
+ version: '5.0'
48
34
  type: :development
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
38
  - - "~>"
53
39
  - !ruby/object:Gem::Version
54
- version: '5.11'
40
+ version: '5.0'
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: minitest-power_assert
57
43
  requirement: !ruby/object:Gem::Requirement
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: 0.3.0
47
+ version: '0.3'
62
48
  type: :development
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: 0.3.0
54
+ version: '0.3'
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: minitest-reporters
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -86,29 +72,15 @@ dependencies:
86
72
  requirements:
87
73
  - - "~>"
88
74
  - !ruby/object:Gem::Version
89
- version: '10.0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '10.0'
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - "~>"
102
- - !ruby/object:Gem::Version
103
- version: 0.54.0
75
+ version: '13.0'
104
76
  type: :development
105
77
  prerelease: false
106
78
  version_requirements: !ruby/object:Gem::Requirement
107
79
  requirements:
108
80
  - - "~>"
109
81
  - !ruby/object:Gem::Version
110
- version: 0.54.0
111
- description:
82
+ version: '13.0'
83
+ description:
112
84
  email: chocolate@cpan.org
113
85
  executables: []
114
86
  extensions: []
@@ -118,6 +90,7 @@ files:
118
90
  - LICENSE.md
119
91
  - README.md
120
92
  - lib/string_splitter.rb
93
+ - lib/string_splitter/split.rb
121
94
  - lib/string_splitter/version.rb
122
95
  homepage: https://github.com/chocolateboy/string_splitter
123
96
  licenses:
@@ -127,7 +100,7 @@ metadata:
127
100
  bug_tracker_uri: https://github.com/chocolateboy/string_splitter/issues
128
101
  changelog_uri: https://github.com/chocolateboy/string_splitter/blob/master/CHANGELOG.md
129
102
  source_code_uri: https://github.com/chocolateboy/string_splitter
130
- post_install_message:
103
+ post_install_message:
131
104
  rdoc_options: []
132
105
  require_paths:
133
106
  - lib
@@ -135,16 +108,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
135
108
  requirements:
136
109
  - - ">="
137
110
  - !ruby/object:Gem::Version
138
- version: '0'
111
+ version: '2.3'
139
112
  required_rubygems_version: !ruby/object:Gem::Requirement
140
113
  requirements:
141
114
  - - ">="
142
115
  - !ruby/object:Gem::Version
143
116
  version: '0'
144
117
  requirements: []
145
- rubyforge_project:
146
- rubygems_version: 2.7.7
147
- signing_key:
118
+ rubygems_version: 3.1.4
119
+ signing_key:
148
120
  specification_version: 4
149
121
  summary: String#split on steroids
150
122
  test_files: []