multi_range 2.2.0 → 2.2.2

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: d15ee2b69e3bd9f58929f35e5e85107e873739b6ecd9499095262e5463b8c733
4
- data.tar.gz: f641524f185a2f759c17cd85a640ac00d825363f9d62b71e1ac2348a878a6717
3
+ metadata.gz: 8f1ca3c6c80f92c3cbbd68f289ffd138c1d988bc33cd506e9eded313b0244504
4
+ data.tar.gz: e27a2de532a9b307e1f1ca7cfe8eff18fbe3af7e607f11291449eb22ec66ad42
5
5
  SHA512:
6
- metadata.gz: f8fcafe38be61f3a971578e4f35bae4751f12e15b550531e7867efba0b982bafee5fecaba53674de6db66d99e6355f65b2a751387eb63668b306d17635b9a01f
7
- data.tar.gz: 818da2c0e7c4e3ce7d07764de88325ffa9bcd3e574d0a73bc46af46b8f7accc3273638ec4704224c5600d087539464b0ca5547907f861bf9eb3aba202e9fb708
6
+ metadata.gz: e3623cbe2b34d20ca94b5a15848fc579a597e5381cee8f2245d5e053ef692e4511821772cc4461d62d7e0ea7bc4cc55613468e5df166d5f56bf68ac91c55b7e4
7
+ data.tar.gz: c04934e7ddc717db16902fea4846b3aabf531b1ea2c8e015f8bf754d8d9dac8abf5cdd4b1bf4c6770752fc35ad02ecc5e9969ca35c05b73fffa8302c1847f779
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Change Log
2
2
 
3
+ ### [v2.2.1](https://github.com/khiav223577/multi_range/compare/v2.2.0...v2.2.1) 2023/10/28
4
+ - [#35](https://github.com/khiav223577/multi_range/pull/35) Fix: intersection with excluded empty range contains extra element (@khiav223577)
5
+
6
+ ### [v2.2.0](https://github.com/khiav223577/multi_range/compare/v2.1.1...v2.2.0) 2023/10/21
7
+ - [#34](https://github.com/khiav223577/multi_range/pull/34) Fix: wrong empty range check which causes some differences to be dropped (@khiav223577)
8
+ - [#33](https://github.com/khiav223577/multi_range/pull/33) Fix: result should not be empty when intersection with inclusive range with one element (@khiav223577)
9
+ - [#32](https://github.com/khiav223577/multi_range/pull/32) Drop the support of ruby 2.2 (@khiav223577)
10
+
3
11
  ### [v2.1.1](https://github.com/khiav223577/multi_range/compare/v2.1.0...v2.1.1) 2021/08/07
4
12
  - [#26](https://github.com/khiav223577/multi_range/pull/26) Fix: unexpected float value when sample an one-element range (@khiav223577)
5
13
 
data/README.md CHANGED
@@ -9,9 +9,8 @@
9
9
  ## Supports
10
10
 
11
11
  - Ruby 2.3 ~ 2.7
12
-
13
- For Ruby 1.8.x and 1.9.x, please use multi_range < 2.
14
- For Ruby 2.0 ~ 2.2, please use multi_range <= 2.1.
12
+ - For Ruby 1.8.x and 1.9.x, please use multi_range < v2.0.0
13
+ - For Ruby 2.0 ~ 2.2, please use multi_range < v2.2.0
15
14
 
16
15
  ## Installation
17
16
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class MultiRange
4
- VERSION = '2.2.0'
4
+ VERSION = '2.2.2'
5
5
  end
data/lib/multi_range.rb CHANGED
@@ -1,242 +1,251 @@
1
- # frozen_string_literal: true
2
-
3
- require 'multi_range/version'
4
- require 'roulette-wheel-selection'
5
- require 'interval_tree'
6
-
7
- if not Range.method_defined?(:size)
8
- warn "Please backports Range#size method to use multi_range gem.\n" \
9
- "You can use backports gem and add the following lines to your program:\n" \
10
- "require 'backports/1.9.2/float/infinity'\n" \
11
- "require 'backports/2.0.0/range/size'"
12
- end
13
-
14
- if not Enumerable.method_defined?(:to_h)
15
- warn "Please backports Enumerable#to_h method to use multi_range gem.\n" \
16
- "You can use backports gem and add the following lines to your program:\n" \
17
- "require 'backports/2.1.0/enumerable/to_h'"
18
- end
19
-
20
- class MultiRange
21
- INDEX_WITH_DEFAULT = Object.new
22
-
23
- attr_reader :ranges
24
-
25
- def initialize(ranges)
26
- if ranges.is_a? MultiRange
27
- @ranges = ranges.ranges
28
- @is_float = ranges.is_float?
29
- else
30
- @ranges = ranges.map{|s| s.is_a?(Numeric) ? s..s : s }.sort_by(&:begin).freeze
31
- @is_float = @ranges.any?{|range| range.begin.is_a?(Float) || range.end.is_a?(Float) }
32
- end
33
- end
34
-
35
- def is_float?
36
- @is_float
37
- end
38
-
39
- def merge_overlaps(merge_same_value = true)
40
- return MultiRange.new([]) if @ranges.size == 0
41
-
42
- new_ranges = []
43
- current_range = nil
44
-
45
- @ranges.each do |range|
46
- next current_range = range if current_range == nil
47
- next if range.end <= current_range.end
48
-
49
- if can_combine?(current_range, range, merge_same_value)
50
- current_range = range.exclude_end? ? current_range.begin...range.end : current_range.begin..range.end
51
- else
52
- new_ranges << current_range
53
- current_range = range
54
- end
55
- end
56
-
57
- new_ranges << current_range
58
- return MultiRange.new(new_ranges)
59
- end
60
-
61
- def &(other)
62
- other_ranges = MultiRange.new(other).merge_overlaps.ranges
63
- tree = IntervalTree::Tree.new(other_ranges)
64
- intersected_ranges = merge_overlaps.ranges.flat_map do |range|
65
- # A workaround for the issue: https://github.com/greensync/interval-tree/issues/17
66
- query = (range.first == range.last) ? range.first : range
67
- matching_ranges_converted_to_exclusive = tree.search(query) || []
68
-
69
- # The interval tree converts interval endings to exclusive, so we need to restore the original
70
- matching_ranges = matching_ranges_converted_to_exclusive.map do |matching_range_converted_to_exclusive|
71
- other_ranges.find do |other_range|
72
- # Having merged overlaps in each multi_range, there's no need to check the endings,
73
- # since there will only be one range with each beginning
74
- other_range.begin == matching_range_converted_to_exclusive.begin
75
- end
76
- end
77
-
78
- matching_ranges.map do |matching_range|
79
- intersect_two_ranges(range, matching_range)
80
- end
81
- end
82
- MultiRange.new(intersected_ranges)
83
- end
84
-
85
- alias intersection &
86
-
87
- def -(other)
88
- return difference_with_other_multi_range(other) if other.is_a?(MultiRange)
89
-
90
- new_ranges = @ranges.dup
91
- return MultiRange.new(new_ranges) if not overlaps_with_range?(other)
92
-
93
- changed_size = 0
94
- @ranges.each_with_index do |range, idx|
95
- # when this range is smaller than and not overlaps with `other`
96
- # range other
97
- # |---------| |---------|
98
- next if other.begin > range.end
99
-
100
- # when this range is larger than and not overlaps with `other`
101
- # other range
102
- # |---------| |---------|
103
- break if other.end < range.begin
104
-
105
- sub_ranges = possible_sub_ranges_of(range, other)
106
- new_ranges[idx + changed_size, 1] = sub_ranges
107
- changed_size += sub_ranges.size - 1
108
-
109
- # when the maximum value of this range is larger than that of `other`
110
- # range
111
- # -------------|
112
- # other
113
- # ---------|
114
- break if other.end <= range.end
115
- end
116
-
117
- return MultiRange.new(new_ranges)
118
- end
119
-
120
- alias difference -
121
-
122
- def |(other)
123
- other_ranges = other.is_a?(MultiRange) ? other.ranges : [other]
124
- return MultiRange.new(@ranges + other_ranges).merge_overlaps
125
- end
126
-
127
- alias union |
128
-
129
- def overlaps?(other)
130
- multi_range = merge_overlaps
131
- return multi_range.ranges != (multi_range - other).ranges
132
- end
133
-
134
- def sample
135
- range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h)
136
- return nil if range == nil
137
- return rand(range)
138
- end
139
-
140
- def size
141
- @ranges.inject(0){|sum, v| sum + v.size }
142
- end
143
-
144
- def any?
145
- @ranges.any?
146
- end
147
-
148
- def index_with(default = INDEX_WITH_DEFAULT)
149
- if block_given?
150
- fail ArgumentError, 'wrong number of arguments (given 1, expected 0)' if default != INDEX_WITH_DEFAULT
151
- return map{|s| [s, yield(s)] }.to_h
152
- end
153
-
154
- return to_enum(:index_with){ size } if default == INDEX_WITH_DEFAULT
155
- return map{|s| [s, default] }.to_h
156
- end
157
-
158
- def each
159
- return to_enum(:each){ size } if !block_given?
160
-
161
- ranges.each do |range|
162
- range.each{|s| yield(s) }
163
- end
164
- end
165
-
166
- def map
167
- return to_enum(:map){ size } if !block_given?
168
- return each.map{|s| yield(s) }
169
- end
170
-
171
- def to_a
172
- each.to_a
173
- end
174
-
175
- def min
176
- range = @ranges.first
177
- return range.min if range
178
- end
179
-
180
- def max
181
- range = @ranges.last
182
- return range.max if range
183
- end
184
-
185
- def contain_overlaps?
186
- merge_overlaps(false).ranges != ranges
187
- end
188
-
189
- private
190
-
191
- # make sure that range1.begin <= range2.begin
192
- def can_combine?(range1, range2, merge_same_value)
193
- return merge_same_value if range1.end == range2.begin and range1.exclude_end?
194
- return range1.end >= range2.begin if @is_float
195
- return range1.end + 1 >= range2.begin
196
- end
197
-
198
- def difference_with_other_multi_range(other)
199
- new_multi_range = dup
200
- other.ranges.each{|range| new_multi_range -= range }
201
- return new_multi_range
202
- end
203
-
204
- def possible_sub_ranges_of(range, other)
205
- sub_range1 = range.begin...other.begin
206
-
207
- sub_range2_begin = if other.exclude_end?
208
- other.end
209
- else
210
- other.end.is_a?(Float) ? other.end.next_float : other.end + 1
211
- end
212
-
213
- sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end
214
-
215
- sub_ranges = []
216
- sub_ranges << sub_range1 if not empty_range?(sub_range1)
217
- sub_ranges << sub_range2 if not empty_range?(sub_range2)
218
- return sub_ranges
219
- end
220
-
221
- def overlaps_with_range?(range)
222
- return false if @ranges.empty?
223
- return false if range.begin > @ranges.last.end # larger than maximum
224
- return false if range.end < @ranges.first.begin # smaller than minimum
225
- return true
226
- end
227
-
228
- def intersect_two_ranges(range_a, range_b)
229
- ranges = [range_a, range_b]
230
- start = ranges.map(&:begin).max
231
- finish = ranges.map(&:end).min
232
- if ranges.sort_by{|range| [range.end, range.exclude_end? ? 1 : 0] }.first.exclude_end?
233
- start...finish
234
- else
235
- start..finish
236
- end
237
- end
238
-
239
- def empty_range?(range)
240
- range.begin > range.end || (range.begin == range.end && range.exclude_end?)
241
- end
242
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_range/version'
4
+ require 'roulette-wheel-selection'
5
+ require 'interval_tree'
6
+
7
+ if not Range.method_defined?(:size)
8
+ warn "Please backports Range#size method to use multi_range gem.\n" \
9
+ "You can use backports gem and add the following lines to your program:\n" \
10
+ "require 'backports/1.9.2/float/infinity'\n" \
11
+ "require 'backports/2.0.0/range/size'"
12
+ end
13
+
14
+ if not Enumerable.method_defined?(:to_h)
15
+ warn "Please backports Enumerable#to_h method to use multi_range gem.\n" \
16
+ "You can use backports gem and add the following lines to your program:\n" \
17
+ "require 'backports/2.1.0/enumerable/to_h'"
18
+ end
19
+
20
+ class MultiRange
21
+ INDEX_WITH_DEFAULT = Object.new
22
+
23
+ attr_reader :ranges
24
+
25
+ def initialize(ranges)
26
+ if ranges.is_a? MultiRange
27
+ @ranges = ranges.ranges
28
+ @is_float = ranges.is_float?
29
+ else
30
+ ranges = [ranges] if !ranges.is_a?(Array)
31
+ @ranges = ranges.map{|s| s.is_a?(Numeric) ? s..s : s }.sort_by(&:begin).freeze
32
+ @is_float = @ranges.any?{|range| range.begin.is_a?(Float) || range.end.is_a?(Float) }
33
+ end
34
+ end
35
+
36
+ def is_float?
37
+ @is_float
38
+ end
39
+
40
+ def merge_overlaps(merge_same_value = true)
41
+ return MultiRange.new([]) if @ranges.size == 0
42
+
43
+ new_ranges = []
44
+ current_range = nil
45
+
46
+ @ranges.each do |range|
47
+ next current_range = range if current_range == nil
48
+ next if range.end <= current_range.end
49
+
50
+ if can_combine?(current_range, range, merge_same_value)
51
+ current_range = range.exclude_end? ? current_range.begin...range.end : current_range.begin..range.end
52
+ else
53
+ new_ranges << current_range
54
+ current_range = range
55
+ end
56
+ end
57
+
58
+ new_ranges << current_range
59
+ return MultiRange.new(new_ranges)
60
+ end
61
+
62
+ def &(other)
63
+ other_ranges = MultiRange.new(other).merge_overlaps.ranges
64
+ tree = IntervalTree::Tree.new(other_ranges)
65
+ intersected_ranges = merge_overlaps.ranges.flat_map do |range|
66
+ # A workaround for the issue: https://github.com/greensync/interval-tree/issues/17
67
+ query = (range.first == range.last) ? range.first : range
68
+ matching_ranges_converted_to_exclusive = tree.search(query) || []
69
+
70
+ # The interval tree converts interval endings to exclusive, so we need to restore the original
71
+ matching_ranges = matching_ranges_converted_to_exclusive.map do |matching_range_converted_to_exclusive|
72
+ other_ranges.find do |other_range|
73
+ # Having merged overlaps in each multi_range, there's no need to check the endings,
74
+ # since there will only be one range with each beginning
75
+ other_range.begin == matching_range_converted_to_exclusive.begin
76
+ end
77
+ end
78
+
79
+ matching_ranges.map do |matching_range|
80
+ intersect_two_ranges(range, matching_range)
81
+ end
82
+ end
83
+ MultiRange.new(intersected_ranges)
84
+ end
85
+
86
+ alias intersection &
87
+
88
+ def -(other)
89
+ return difference_with_other_multi_range(other) if other.is_a?(MultiRange)
90
+
91
+ new_ranges = @ranges.dup
92
+ return MultiRange.new(new_ranges) if not overlaps_with_range?(other)
93
+
94
+ changed_size = 0
95
+ @ranges.each_with_index do |range, idx|
96
+ # when this range is smaller than and not overlaps with `other`
97
+ # range other
98
+ # |---------| |---------|
99
+ next if other.begin > range.end
100
+
101
+ # when this range is larger than and not overlaps with `other`
102
+ # other range
103
+ # |---------| |---------|
104
+ break if other.end < range.begin
105
+
106
+ sub_ranges = possible_sub_ranges_of(range, other)
107
+ new_ranges[idx + changed_size, 1] = sub_ranges
108
+ changed_size += sub_ranges.size - 1
109
+
110
+ # when the maximum value of this range is larger than that of `other`
111
+ # range
112
+ # -------------|
113
+ # other
114
+ # ---------|
115
+ break if other.end <= range.end
116
+ end
117
+
118
+ return MultiRange.new(new_ranges)
119
+ end
120
+
121
+ alias difference -
122
+
123
+ def |(other)
124
+ other_ranges = other.is_a?(MultiRange) ? other.ranges : [other]
125
+ return MultiRange.new(@ranges + other_ranges).merge_overlaps
126
+ end
127
+
128
+ alias union |
129
+
130
+ def overlaps?(other)
131
+ multi_range = merge_overlaps
132
+ return multi_range.ranges != (multi_range - other).ranges
133
+ end
134
+
135
+ def sample
136
+ range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h)
137
+ return nil if range == nil
138
+ return rand(range)
139
+ end
140
+
141
+ def size
142
+ @ranges.inject(0){|sum, v| sum + v.size }
143
+ end
144
+
145
+ def any?
146
+ @ranges.any?
147
+ end
148
+
149
+ def index_with(default = INDEX_WITH_DEFAULT)
150
+ if block_given?
151
+ fail ArgumentError, 'wrong number of arguments (given 1, expected 0)' if default != INDEX_WITH_DEFAULT
152
+ return map{|s| [s, yield(s)] }.to_h
153
+ end
154
+
155
+ return to_enum(:index_with){ size } if default == INDEX_WITH_DEFAULT
156
+ return map{|s| [s, default] }.to_h
157
+ end
158
+
159
+ def each
160
+ return to_enum(:each){ size } if !block_given?
161
+
162
+ ranges.each do |range|
163
+ range.each{|s| yield(s) }
164
+ end
165
+ end
166
+
167
+ def map
168
+ return to_enum(:map){ size } if !block_given?
169
+ return each.map{|s| yield(s) }
170
+ end
171
+
172
+ def to_a
173
+ each.to_a
174
+ end
175
+
176
+ def min
177
+ range = @ranges.first
178
+ return range.min if range
179
+ end
180
+
181
+ def max
182
+ range = @ranges.last
183
+ return range.max if range
184
+ end
185
+
186
+ def contain_overlaps?
187
+ merge_overlaps(false).ranges != ranges
188
+ end
189
+
190
+ private
191
+
192
+ # make sure that range1.begin <= range2.begin
193
+ def can_combine?(range1, range2, merge_same_value)
194
+ return merge_same_value if range1.end == range2.begin and range1.exclude_end?
195
+ return range1.end >= range2.begin if @is_float
196
+ return range1.end + 1 >= range2.begin
197
+ end
198
+
199
+ def difference_with_other_multi_range(other)
200
+ new_multi_range = dup
201
+ other.ranges.each{|range| new_multi_range -= range }
202
+ return new_multi_range
203
+ end
204
+
205
+ def possible_sub_ranges_of(range, other)
206
+ sub_range1 = range.begin...other.begin
207
+
208
+ sub_range2_begin = if other.exclude_end?
209
+ other.end
210
+ else
211
+ other.end.is_a?(Float) ? other.end.next_float : other.end + 1
212
+ end
213
+
214
+ sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end
215
+
216
+ sub_ranges = []
217
+ sub_ranges << sub_range1 if not empty_range?(sub_range1)
218
+ sub_ranges << sub_range2 if not empty_range?(sub_range2)
219
+ return sub_ranges
220
+ end
221
+
222
+ def overlaps_with_range?(range)
223
+ return false if @ranges.empty?
224
+ return false if range.begin > @ranges.last.end # larger than maximum
225
+ return false if range.end < @ranges.first.begin # smaller than minimum
226
+ return true
227
+ end
228
+
229
+ def intersect_two_ranges(range1, range2)
230
+ start_range = range_with_larger_start(range1, range2)
231
+ end_range = range_with_smaller_end(range1, range2)
232
+ return start_range.begin...end_range.end if end_range.exclude_end?
233
+ return start_range.begin..end_range.end
234
+ end
235
+
236
+ def empty_range?(range)
237
+ range.begin > range.end || (range.begin == range.end && range.exclude_end?)
238
+ end
239
+
240
+ def range_with_larger_start(range1, range2)
241
+ return range1 if range1.begin > range2.begin
242
+ return range2
243
+ end
244
+
245
+ def range_with_smaller_end(range1, range2)
246
+ return range1 if range1.end < range2.end
247
+ return range2 if range1.end > range2.end
248
+ return range1 if range1.exclude_end?
249
+ return range2
250
+ end
251
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multi_range
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - khiav reoy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-21 00:00:00.000000000 Z
11
+ date: 2023-12-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler