multi_range 2.2.0 → 2.2.1

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: 2d67638eab33c3a377488c4fb32c0e33f8385eab6d743d3f37f7476f20f94b97
4
+ data.tar.gz: 198971530fc824d45690f92b4c45013a8bd58ddb204e9a84a2971bdda20ed1fb
5
5
  SHA512:
6
- metadata.gz: f8fcafe38be61f3a971578e4f35bae4751f12e15b550531e7867efba0b982bafee5fecaba53674de6db66d99e6355f65b2a751387eb63668b306d17635b9a01f
7
- data.tar.gz: 818da2c0e7c4e3ce7d07764de88325ffa9bcd3e574d0a73bc46af46b8f7accc3273638ec4704224c5600d087539464b0ca5547907f861bf9eb3aba202e9fb708
6
+ metadata.gz: 752b8eff047ed5b7dded92afbdf612fc0e2f63b119fba141421d07e2983d8fd9aa79914cb60e890414362d6e84497b2737145dc18166299f772a74e74ffb231d
7
+ data.tar.gz: 48e47778eaf9066be35983239fdf073e011010ba2555d1a2b4a48d4c507db7e9d419a64503f469a54e16363be61e5e300977f364d02c91f12d56ad3028ef315f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## Change Log
2
2
 
3
+ ### [v2.2.0](https://github.com/khiav223577/multi_range/compare/v1.3.2...v2.2.0) 2023/10/21
4
+ - [#34](https://github.com/khiav223577/multi_range/pull/34) Fix: wrong empty range check which causes some differences to be dropped (@khiav223577)
5
+ - [#33](https://github.com/khiav223577/multi_range/pull/33) Fix: result should not be empty when intersection with inclusive range with one element (@khiav223577)
6
+ - [#32](https://github.com/khiav223577/multi_range/pull/32) Drop the support of ruby 2.2 (@khiav223577)
7
+
3
8
  ### [v2.1.1](https://github.com/khiav223577/multi_range/compare/v2.1.0...v2.1.1) 2021/08/07
4
9
  - [#26](https://github.com/khiav223577/multi_range/pull/26) Fix: unexpected float value when sample an one-element range (@khiav223577)
5
10
 
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.1'
5
5
  end
data/lib/multi_range.rb CHANGED
@@ -1,242 +1,250 @@
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.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(range1, range2)
229
+ start_range = range_with_larger_start(range1, range2)
230
+ end_range = range_with_smaller_end(range1, range2)
231
+ return start_range.begin...end_range.end if end_range.exclude_end?
232
+ return start_range.begin..end_range.end
233
+ end
234
+
235
+ def empty_range?(range)
236
+ range.begin > range.end || (range.begin == range.end && range.exclude_end?)
237
+ end
238
+
239
+ def range_with_larger_start(range1, range2)
240
+ return range1 if range1.begin > range2.begin
241
+ return range2
242
+ end
243
+
244
+ def range_with_smaller_end(range1, range2)
245
+ return range1 if range1.end < range2.end
246
+ return range2 if range1.end > range2.end
247
+ return range1 if range1.exclude_end?
248
+ return range2
249
+ end
250
+ 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.1
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-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler