multi_range 2.2.0 → 2.2.1

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