multi_range 1.3.2 → 2.0.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/lib/multi_range.rb CHANGED
@@ -1,174 +1,222 @@
1
- # frozen_string_literal: true
2
-
3
- require 'multi_range/version'
4
- require 'roulette-wheel-selection'
5
-
6
- if not Range.method_defined?(:size)
7
- warn "Please backports Range#size method to use multi_range gem.\n" \
8
- "You can use backports gem and add the following lines to your program:\n" \
9
- "require 'backports/1.9.2/float/infinity'\n" \
10
- "require 'backports/2.0.0/range/size'"
11
- end
12
-
13
- if not Enumerable.method_defined?(:to_h)
14
- warn "Please backports Enumerable#to_h method to use multi_range gem.\n" \
15
- "You can use backports gem and add the following lines to your program:\n" \
16
- "require 'backports/2.1.0/enumerable/to_h'"
17
- end
18
-
19
- class MultiRange
20
- INDEX_WITH_DEFAULT = Object.new
21
-
22
- attr_reader :ranges
23
-
24
- def initialize(ranges)
25
- @ranges = ranges.map{|s| s.is_a?(Numeric) ? s..s : s }.sort_by{|s| s.begin }.freeze
26
- @is_float = @ranges.any?{|range| range.begin.is_a?(Float) || range.end.is_a?(Float) }
27
- end
28
-
29
- def merge_overlaps(merge_same_value = true)
30
- return MultiRange.new([]) if @ranges.size == 0
31
-
32
- new_ranges = []
33
- current_range = nil
34
-
35
- @ranges.each do |range|
36
- next current_range = range if current_range == nil
37
- next if range.end <= current_range.end
38
-
39
- if can_combine?(current_range, range, merge_same_value)
40
- current_range = range.exclude_end? ? current_range.begin...range.end : current_range.begin..range.end
41
- else
42
- new_ranges << current_range
43
- current_range = range
44
- end
45
- end
46
-
47
- new_ranges << current_range
48
- return MultiRange.new(new_ranges)
49
- end
50
-
51
- def -(other)
52
- return difference_with_other_multi_range(other) if other.is_a?(MultiRange)
53
-
54
- new_ranges = @ranges.dup
55
- return MultiRange.new(new_ranges) if not overlaps_with_range?(other)
56
-
57
- changed_size = 0
58
- @ranges.each_with_index do |range, idx|
59
- next if other.begin > range.end # 大於這個 range
60
- break if other.end < range.begin # 小於這個 range
61
-
62
- sub_ranges = possible_sub_ranges_of(range, other)
63
- new_ranges[idx + changed_size, 1] = sub_ranges
64
- changed_size += sub_ranges.size - 1
65
- break if other.end <= range.end # 沒有超過一個 range 的範圍
66
- end
67
-
68
- return MultiRange.new(new_ranges)
69
- end
70
-
71
- def |(other)
72
- other_ranges = other.is_a?(MultiRange) ? other.ranges : [other]
73
- return MultiRange.new(@ranges + other_ranges).merge_overlaps
74
- end
75
-
76
- def overlaps?(other)
77
- multi_range = merge_overlaps
78
- return multi_range.ranges != (multi_range - other).ranges
79
- end
80
-
81
- def sample
82
- range = RouletteWheelSelection.sample(@ranges.map{|s| [s, s.size] }.to_h)
83
- return nil if range == nil
84
- return rand(range.max - range.min + 1) + range.min
85
- end
86
-
87
- def size
88
- @ranges.inject(0){|sum, v| sum + v.size }
89
- end
90
-
91
- def any?
92
- @ranges.any?
93
- end
94
-
95
- def index_with(default = INDEX_WITH_DEFAULT)
96
- if block_given?
97
- fail ArgumentError, 'wrong number of arguments (given 1, expected 0)' if default != INDEX_WITH_DEFAULT
98
- return map{|s| [s, yield(s)] }.to_h
99
- end
100
-
101
- return to_enum(:index_with){ size } if default == INDEX_WITH_DEFAULT
102
- return map{|s| [s, default] }.to_h
103
- end
104
-
105
- def each
106
- return to_enum(:each){ size } if !block_given?
107
-
108
- ranges.each do |range|
109
- range.each{|s| yield(s) }
110
- end
111
- end
112
-
113
- def map
114
- return to_enum(:map){ size } if !block_given?
115
- return each.map{|s| yield(s) }
116
- end
117
-
118
- def to_a
119
- each.to_a
120
- end
121
-
122
- def min
123
- range = @ranges.first
124
- return range.min if range
125
- end
126
-
127
- def max
128
- range = @ranges.last
129
- return range.max if range
130
- end
131
-
132
- def contain_overlaps?
133
- merge_overlaps(false).ranges != ranges
134
- end
135
-
136
- private
137
-
138
- # make sure that range1.begin <= range2.begin
139
- def can_combine?(range1, range2, merge_same_value)
140
- return merge_same_value if range1.end == range2.begin and range1.exclude_end?
141
- return range1.end >= range2.begin if @is_float
142
- return range1.end + 1 >= range2.begin
143
- end
144
-
145
- def difference_with_other_multi_range(other)
146
- new_multi_range = dup
147
- other.ranges.each{|range| new_multi_range -= range }
148
- return new_multi_range
149
- end
150
-
151
- def possible_sub_ranges_of(range, other)
152
- sub_range1 = range.begin...other.begin
153
-
154
- sub_range2_begin = if other.exclude_end?
155
- other.end
156
- else
157
- other.end + (other.end.is_a?(Float) ? Float::EPSILON : 1)
158
- end
159
-
160
- sub_range2 = range.exclude_end? ? sub_range2_begin...range.end : sub_range2_begin..range.end
161
-
162
- sub_ranges = []
163
- sub_ranges << sub_range1 if sub_range1.begin <= sub_range1.end
164
- sub_ranges << sub_range2 if sub_range2.begin <= sub_range2.end
165
- return sub_ranges
166
- end
167
-
168
- def overlaps_with_range?(range)
169
- return false if @ranges.empty?
170
- return false if range.begin > @ranges.last.end # larger than maxinum
171
- return false if range.end < @ranges.first.begin # smaller than mininum
172
- return true
173
- end
174
- 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 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
data/multi_range.gemspec CHANGED
@@ -1,43 +1,47 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'multi_range/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = 'multi_range'
8
- spec.version = MultiRange::VERSION
9
- spec.authors = ['khiav reoy']
10
- spec.email = ['mrtmrt15xn@yahoo.com.tw']
11
-
12
- spec.summary = 'Allow you to manipulate a group of ranges. Such as merging overlapping ranges, doing ranges union and difference.'
13
- spec.description = 'Allow you to manipulate a group of ranges. Such as merging overlapping ranges, doing ranges union and difference.'
14
- spec.homepage = 'https://github.com/khiav223577/multi_range'
15
- spec.license = 'MIT'
16
-
17
- # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
- # delete this section to allow pushing this gem to any host.
19
- # if spec.respond_to?(:metadata)
20
- # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
- # else
22
- # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
- # end
24
-
25
- spec.files = `git ls-files -z`.split("\x0").reject{|f| f.match(%r{^(test|spec|features)/}) }
26
- spec.bindir = 'exe'
27
- spec.executables = spec.files.grep(%r{^exe/}){|f| File.basename(f) }
28
- spec.require_paths = ['lib']
29
- spec.metadata = {
30
- 'homepage_uri' => 'https://github.com/khiav223577/multi_range',
31
- 'changelog_uri' => 'https://github.com/khiav223577/multi_range/blob/master/CHANGELOG.md',
32
- 'source_code_uri' => 'https://github.com/khiav223577/multi_range',
33
- 'documentation_uri' => 'https://www.rubydoc.info/gems/multi_range',
34
- 'bug_tracker_uri' => 'https://github.com/khiav223577/multi_range/issues',
35
- }
36
-
37
- spec.add_development_dependency 'bundler', '>= 1.17', '< 3.x'
38
- spec.add_development_dependency 'rake', '>= 10.5.0'
39
- spec.add_development_dependency 'minitest', '~> 5.0'
40
- spec.add_development_dependency 'backports', '~> 3.15.0'
41
-
42
- spec.add_dependency 'roulette-wheel-selection', '~> 1.1.1'
43
- end
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'multi_range/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'multi_range'
8
+ spec.version = MultiRange::VERSION
9
+ spec.authors = ['khiav reoy']
10
+ spec.email = ['mrtmrt15xn@yahoo.com.tw']
11
+
12
+ spec.summary = 'Allow you to manipulate a group of ranges. Such as merging overlapping ranges, doing ranges union and difference.'
13
+ spec.description = 'Allow you to manipulate a group of ranges. Such as merging overlapping ranges, doing ranges union and difference.'
14
+ spec.homepage = 'https://github.com/khiav223577/multi_range'
15
+ spec.license = 'MIT'
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ # end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject{|f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}){|f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+ spec.metadata = {
30
+ 'homepage_uri' => 'https://github.com/khiav223577/multi_range',
31
+ 'changelog_uri' => 'https://github.com/khiav223577/multi_range/blob/master/CHANGELOG.md',
32
+ 'source_code_uri' => 'https://github.com/khiav223577/multi_range',
33
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/multi_range',
34
+ 'bug_tracker_uri' => 'https://github.com/khiav223577/multi_range/issues',
35
+ }
36
+
37
+ spec.required_ruby_version = '>= 2.0'
38
+
39
+ spec.add_development_dependency 'bundler', '>= 1.17', '< 3.x'
40
+ spec.add_development_dependency 'rake', '>= 10.5.0'
41
+ spec.add_development_dependency 'minitest', '~> 5.0'
42
+ spec.add_development_dependency 'minitest-color', '~> 0.0.2'
43
+ spec.add_development_dependency 'backports', '~> 3.15.0'
44
+
45
+ spec.add_dependency 'roulette-wheel-selection', '~> 1.1.1'
46
+ spec.add_dependency 'fast_interval_tree', '~> 0.2.0'
47
+ 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: 1.3.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - khiav reoy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-07 00:00:00.000000000 Z
11
+ date: 2020-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,6 +58,20 @@ dependencies:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
60
  version: '5.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: minitest-color
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 0.0.2
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.0.2
61
75
  - !ruby/object:Gem::Dependency
62
76
  name: backports
63
77
  requirement: !ruby/object:Gem::Requirement
@@ -86,6 +100,20 @@ dependencies:
86
100
  - - "~>"
87
101
  - !ruby/object:Gem::Version
88
102
  version: 1.1.1
103
+ - !ruby/object:Gem::Dependency
104
+ name: fast_interval_tree
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.2.0
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.2.0
89
117
  description: Allow you to manipulate a group of ranges. Such as merging overlapping
90
118
  ranges, doing ranges union and difference.
91
119
  email:
@@ -105,7 +133,6 @@ files:
105
133
  - bin/console
106
134
  - bin/setup
107
135
  - gemfiles/Gemfile
108
- - gemfiles/ruby_1_8_7.gemfile
109
136
  - lib/multi_range.rb
110
137
  - lib/multi_range/version.rb
111
138
  - multi_range.gemspec
@@ -126,14 +153,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
126
153
  requirements:
127
154
  - - ">="
128
155
  - !ruby/object:Gem::Version
129
- version: '0'
156
+ version: '2.0'
130
157
  required_rubygems_version: !ruby/object:Gem::Requirement
131
158
  requirements:
132
159
  - - ">="
133
160
  - !ruby/object:Gem::Version
134
161
  version: '0'
135
162
  requirements: []
136
- rubygems_version: 3.2.14
163
+ rubygems_version: 3.0.3
137
164
  signing_key:
138
165
  specification_version: 4
139
166
  summary: Allow you to manipulate a group of ranges. Such as merging overlapping ranges,
@@ -1,10 +0,0 @@
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
- gem 'minitest', '~> 5.11.3'
8
- end
9
-
10
- gemspec :path => '../'