multi_range 2.0.0 → 2.1.0

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.
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
- require 'bundler/gem_tasks'
2
- require 'rake/testtask'
3
-
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << 'test'
6
- t.libs << 'lib'
7
- t.test_files = FileList['test/**/*_test.rb']
8
- end
9
-
10
- task :default => :test
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console CHANGED
@@ -1,14 +1,14 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'multi_range'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require 'irb'
14
- IRB.start
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'multi_range'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install --gemfile=gemfiles/Gemfile
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install --gemfile=gemfiles/Gemfile
7
+
8
+ # Do any other automated setup that you need to do here
data/gemfiles/Gemfile CHANGED
@@ -1,9 +1,9 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in rails_or.gemspec
4
-
5
- group :test do
6
- gem 'simplecov'
7
- end
8
-
9
- gemspec :path => '../'
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rails_or.gemspec
4
+
5
+ group :test do
6
+ gem 'simplecov', '< 0.18'
7
+ end
8
+
9
+ gemspec :path => '../'
data/lib/multi_range.rb CHANGED
@@ -1,222 +1,236 @@
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
- matching_ranges_converted_to_exclusive = tree.search(range) || []
66
-
67
- # The interval tree converts interval endings to exclusive, so we need to restore the original
68
- matching_ranges = matching_ranges_converted_to_exclusive.map do |matching_range_converted_to_exclusive|
69
- other_ranges.find do |other_range|
70
- # Having merged overlaps in each multirange, there's no need to check the endings, since there will only be one range with each beginning
71
- other_range.begin == matching_range_converted_to_exclusive.begin
72
- end
73
- end
74
-
75
- matching_ranges.map do |matching_range|
76
- intersect_two_ranges(range, matching_range)
77
- end
78
- end
79
- MultiRange.new(intersected_ranges)
80
- end
81
-
82
- alias intersection &
83
-
84
- def -(other)
85
- return difference_with_other_multi_range(other) if other.is_a?(MultiRange)
86
-
87
- new_ranges = @ranges.dup
88
- return MultiRange.new(new_ranges) if not overlaps_with_range?(other)
89
-
90
- changed_size = 0
91
- @ranges.each_with_index do |range, idx|
92
- next if other.begin > range.end # 大於這個 range
93
- break if other.end < range.begin # 小於這個 range
94
-
95
- sub_ranges = possible_sub_ranges_of(range, other)
96
- new_ranges[idx + changed_size, 1] = sub_ranges
97
- changed_size += sub_ranges.size - 1
98
- break if other.end <= range.end # 沒有超過一個 range 的範圍
99
- end
100
-
101
- return MultiRange.new(new_ranges)
102
- end
103
-
104
- alias difference -
105
-
106
- def |(other)
107
- other_ranges = other.is_a?(MultiRange) ? other.ranges : [other]
108
- return MultiRange.new(@ranges + other_ranges).merge_overlaps
109
- end
110
-
111
- alias union |
112
-
113
- def overlaps?(other)
114
- multi_range = merge_overlaps
115
- return multi_range.ranges != (multi_range - other).ranges
116
- end
117
-
118
- def sample
119
- range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h)
120
- return nil if range == nil
121
- return rand(range.max - range.min) + range.min
122
- end
123
-
124
- def size
125
- @ranges.inject(0){|sum, v| sum + v.size }
126
- end
127
-
128
- def any?
129
- @ranges.any?
130
- end
131
-
132
- def index_with(default = INDEX_WITH_DEFAULT)
133
- if block_given?
134
- fail ArgumentError, 'wrong number of arguments (given 1, expected 0)' if default != INDEX_WITH_DEFAULT
135
- return map{|s| [s, yield(s)] }.to_h
136
- end
137
-
138
- return to_enum(:index_with){ size } if default == INDEX_WITH_DEFAULT
139
- return map{|s| [s, default] }.to_h
140
- end
141
-
142
- def each
143
- return to_enum(:each){ size } if !block_given?
144
-
145
- ranges.each do |range|
146
- range.each{|s| yield(s) }
147
- end
148
- end
149
-
150
- def map
151
- return to_enum(:map){ size } if !block_given?
152
- return each.map{|s| yield(s) }
153
- end
154
-
155
- def to_a
156
- each.to_a
157
- end
158
-
159
- def min
160
- range = @ranges.first
161
- return range.min if range
162
- end
163
-
164
- def max
165
- range = @ranges.last
166
- return range.max if range
167
- end
168
-
169
- def contain_overlaps?
170
- merge_overlaps(false).ranges != ranges
171
- end
172
-
173
- private
174
-
175
- # make sure that range1.begin <= range2.begin
176
- def can_combine?(range1, range2, merge_same_value)
177
- return merge_same_value if range1.end == range2.begin and range1.exclude_end?
178
- return range1.end >= range2.begin if @is_float
179
- return range1.end + 1 >= range2.begin
180
- end
181
-
182
- def difference_with_other_multi_range(other)
183
- new_multi_range = dup
184
- other.ranges.each{|range| new_multi_range -= range }
185
- return new_multi_range
186
- end
187
-
188
- def possible_sub_ranges_of(range, other)
189
- sub_range1 = range.begin...other.begin
190
-
191
- sub_range2_begin = if other.exclude_end?
192
- other.end
193
- else
194
- other.end + (other.end.is_a?(Float) ? Float::EPSILON : 1)
195
- end
196
-
197
- sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end
198
-
199
- sub_ranges = []
200
- sub_ranges << sub_range1 if sub_range1.begin <= sub_range1.end
201
- sub_ranges << sub_range2 if sub_range2.begin <= sub_range2.end
202
- return sub_ranges
203
- end
204
-
205
- def overlaps_with_range?(range)
206
- return false if @ranges.empty?
207
- return false if range.begin > @ranges.last.end # larger than maxinum
208
- return false if range.end < @ranges.first.begin # smaller than mininum
209
- return true
210
- end
211
-
212
- def intersect_two_ranges(range_a, range_b)
213
- ranges = [range_a, range_b]
214
- start = ranges.map(&:begin).max
215
- finish = ranges.map(&:end).min
216
- if ranges.sort_by { |range| [range.end, range.exclude_end? ? 1 : 0] }.first.exclude_end?
217
- start...finish
218
- else
219
- start..finish
220
- end
221
- end
222
- 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
+ matching_ranges_converted_to_exclusive = tree.search(range) || []
66
+
67
+ # The interval tree converts interval endings to exclusive, so we need to restore the original
68
+ matching_ranges = matching_ranges_converted_to_exclusive.map do |matching_range_converted_to_exclusive|
69
+ other_ranges.find do |other_range|
70
+ # Having merged overlaps in each multi_range, there's no need to check the endings,
71
+ # since there will only be one range with each beginning
72
+ other_range.begin == matching_range_converted_to_exclusive.begin
73
+ end
74
+ end
75
+
76
+ matching_ranges.map do |matching_range|
77
+ intersect_two_ranges(range, matching_range)
78
+ end
79
+ end
80
+ MultiRange.new(intersected_ranges)
81
+ end
82
+
83
+ alias intersection &
84
+
85
+ def -(other)
86
+ return difference_with_other_multi_range(other) if other.is_a?(MultiRange)
87
+
88
+ new_ranges = @ranges.dup
89
+ return MultiRange.new(new_ranges) if not overlaps_with_range?(other)
90
+
91
+ changed_size = 0
92
+ @ranges.each_with_index do |range, idx|
93
+ # when this range is smaller than and not overlaps with `other`
94
+ # range other
95
+ # |---------| |---------|
96
+ next if other.begin > range.end
97
+
98
+ # when this range is larger than and not overlaps with `other`
99
+ # other range
100
+ # |---------| |---------|
101
+ break if other.end < range.begin
102
+
103
+ sub_ranges = possible_sub_ranges_of(range, other)
104
+ new_ranges[idx + changed_size, 1] = sub_ranges
105
+ changed_size += sub_ranges.size - 1
106
+
107
+ # when the maximum value of this range is larger than that of `other`
108
+ # range
109
+ # -------------|
110
+ # other
111
+ # ---------|
112
+ break if other.end <= range.end
113
+ end
114
+
115
+ return MultiRange.new(new_ranges)
116
+ end
117
+
118
+ alias difference -
119
+
120
+ def |(other)
121
+ other_ranges = other.is_a?(MultiRange) ? other.ranges : [other]
122
+ return MultiRange.new(@ranges + other_ranges).merge_overlaps
123
+ end
124
+
125
+ alias union |
126
+
127
+ def overlaps?(other)
128
+ multi_range = merge_overlaps
129
+ return multi_range.ranges != (multi_range - other).ranges
130
+ end
131
+
132
+ def sample
133
+ range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h)
134
+ return nil if range == nil
135
+ return rand(range.max - range.min) + range.min
136
+ end
137
+
138
+ def size
139
+ @ranges.inject(0){|sum, v| sum + v.size }
140
+ end
141
+
142
+ def any?
143
+ @ranges.any?
144
+ end
145
+
146
+ def index_with(default = INDEX_WITH_DEFAULT)
147
+ if block_given?
148
+ fail ArgumentError, 'wrong number of arguments (given 1, expected 0)' if default != INDEX_WITH_DEFAULT
149
+ return map{|s| [s, yield(s)] }.to_h
150
+ end
151
+
152
+ return to_enum(:index_with){ size } if default == INDEX_WITH_DEFAULT
153
+ return map{|s| [s, default] }.to_h
154
+ end
155
+
156
+ def each
157
+ return to_enum(:each){ size } if !block_given?
158
+
159
+ ranges.each do |range|
160
+ range.each{|s| yield(s) }
161
+ end
162
+ end
163
+
164
+ def map
165
+ return to_enum(:map){ size } if !block_given?
166
+ return each.map{|s| yield(s) }
167
+ end
168
+
169
+ def to_a
170
+ each.to_a
171
+ end
172
+
173
+ def min
174
+ range = @ranges.first
175
+ return range.min if range
176
+ end
177
+
178
+ def max
179
+ range = @ranges.last
180
+ return range.max if range
181
+ end
182
+
183
+ def contain_overlaps?
184
+ merge_overlaps(false).ranges != ranges
185
+ end
186
+
187
+ private
188
+
189
+ # make sure that range1.begin <= range2.begin
190
+ def can_combine?(range1, range2, merge_same_value)
191
+ return merge_same_value if range1.end == range2.begin and range1.exclude_end?
192
+ return range1.end >= range2.begin if @is_float
193
+ return range1.end + 1 >= range2.begin
194
+ end
195
+
196
+ def difference_with_other_multi_range(other)
197
+ new_multi_range = dup
198
+ other.ranges.each{|range| new_multi_range -= range }
199
+ return new_multi_range
200
+ end
201
+
202
+ def possible_sub_ranges_of(range, other)
203
+ sub_range1 = range.begin...other.begin
204
+
205
+ sub_range2_begin = if other.exclude_end?
206
+ other.end
207
+ else
208
+ other.end + (other.end.is_a?(Float) ? Float::EPSILON : 1)
209
+ end
210
+
211
+ sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end
212
+
213
+ sub_ranges = []
214
+ sub_ranges << sub_range1 if sub_range1.begin < sub_range1.end
215
+ sub_ranges << sub_range2 if sub_range2.begin < sub_range2.end
216
+ return sub_ranges
217
+ end
218
+
219
+ def overlaps_with_range?(range)
220
+ return false if @ranges.empty?
221
+ return false if range.begin > @ranges.last.end # larger than maximum
222
+ return false if range.end < @ranges.first.begin # smaller than minimum
223
+ return true
224
+ end
225
+
226
+ def intersect_two_ranges(range_a, range_b)
227
+ ranges = [range_a, range_b]
228
+ start = ranges.map(&:begin).max
229
+ finish = ranges.map(&:end).min
230
+ if ranges.sort_by { |range| [range.end, range.exclude_end? ? 1 : 0] }.first.exclude_end?
231
+ start...finish
232
+ else
233
+ start..finish
234
+ end
235
+ end
236
+ end