multi_range 2.2.0 → 2.2.2

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: 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