extrarange 1.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Timothy Elliott, Alan Franzoni
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,115 @@
1
+ # Extrarange: enhancing the Ruby range experience.
2
+
3
+
4
+ ## SparseRange
5
+
6
+ The SparseRange class lets you easily collect many ranges into one. It accepts
7
+ individual values (3, 4, 5) as well as ranges (2..3000) and will fold them
8
+ together to efficiently work with them as a single set of ranges.
9
+
10
+ Once you have inserted your values and ranges, you can perform logic operations
11
+ such as union and intersection. This is particularly useful for finding
12
+ overlapping time spans.
13
+
14
+ I took care to allow for non-sequential ranges, such as ranges of floats, to
15
+ work with this class. Non-sequential ranges are ranges for data types that don't
16
+ have a succ method.
17
+
18
+ The class aims at being a drop-in replacement of a standard range; it exposes the very
19
+ same interface of the builtin ruby range. There're currently some caveats, check them below.
20
+
21
+ ### Usage
22
+
23
+ Require and include the module
24
+
25
+ require 'extrarange'
26
+ include ExtraRange
27
+
28
+ Initialize a range with gaps, the order doesn't matter, and overlapping values
29
+ are folded together:
30
+
31
+ extrarange = SparseRange.new 55...900, 1..34, 22, 23
32
+
33
+ Add values to a range with gaps:
34
+
35
+ extrarange << 0.4
36
+ extrarange.add(Time.now...(Time.now + 5000))
37
+
38
+ Delete values from a range with gaps:
39
+
40
+ extrarange.delete(Date.today - 15)
41
+ extrarange.delete(50..3000)
42
+
43
+ Fast calculation of the size of a range with gaps:
44
+
45
+ extrarange.size
46
+
47
+ Use logic operations on a range with gaps:
48
+
49
+ # returns a range with gaps of all overlapping ranges
50
+ extrarange_one & extrarange_two
51
+ extrarange & ((Date.today - 30)..Date.today)
52
+
53
+ # returns a range with gaps of all ranges combined
54
+ extrarange_one | extrarange_two
55
+
56
+ I created this class in order to simplify the calculation of billing periods in
57
+ a rails project that uses a audit table:
58
+
59
+ require 'extrarange'
60
+ include ExtraRange
61
+
62
+ def get_premium_time_spans
63
+ premium_time_spans = SparseRange.new
64
+
65
+ # Get all events from an audit table, i.e. every time a customer toggles
66
+ # the premium flag on their account.
67
+ premium_toggles = Customer.audits.premium_column
68
+
69
+ # We only care when premium was on, so eat the first event if it was a
70
+ # toggle to a non-premium account.
71
+ premium_toggles.shift unless premium_toggles.first.value
72
+
73
+ premium_toggles.each_slice(2) do |toggle_pair|
74
+ # If we only got one element in the slice then inject a fake toggle
75
+ # at the end of the span
76
+ span_end = toggle_pair[1] ? toggle_pair[1].created_at : Time.now
77
+
78
+ premium_time_spans.add(toggle_pair[0].created_at..span_end)
79
+ end
80
+
81
+ premium_time_spans
82
+ end
83
+
84
+ premium_time_spans = get_premium_time_spans
85
+
86
+ # Calculate how long the customer had a premium account in January
87
+ january = Time.local(2010, 1, 1)...Time.local(2010, 2, 1)
88
+ puts "January Premium: #{(premium_time_spans & january).size} seconds"
89
+
90
+ # Calculate how long the customer had a premium account during the two last
91
+ # service outages
92
+ disk_recovery = Time.local(2009, 12, 24, 15, 30)..Time.local(2009, 12, 24, 16)
93
+ net_outage = Time.local(2010, 2, 1, 12, 15)..Time.local(2010, 2, 2, 2, 30)
94
+ outages = SparseRange.new disk_recovery, net_outage
95
+
96
+ puts "Outage Premium: #{(premium_time_spans & outages).size} seconds"
97
+
98
+ ### Caveats
99
+ - SparseRange is currently mutable. Be careful whenever using it as an hash key as it may produce unexpected results.
100
+ - If *any* range within the sparse range doesn't support min/max (e.g. reverse ranges like 3..1), then the sparse
101
+ range won't support them as well. I hope to fix such behaviour in a future version.
102
+ - cover? is unoptimized and may take a lot of time in large sparse ranges.
103
+
104
+ ## Credits
105
+
106
+ This is a fork of the [range_with_gaps](https://github.com/ender672/range_with_gaps) gem by Timothy Elliott.
107
+
108
+ ## TODO
109
+
110
+ - Create an immutable version of SparseRange
111
+ - Create another edition of Ruby builtin range, supporting an optional external succ/comparator/whatever we may need to build an
112
+ arbitrary range
113
+ - Create things like cyclic ranges, etc.
114
+
115
+
@@ -0,0 +1,5 @@
1
+ require 'extrarange/range_math'
2
+
3
+ class Range
4
+ include ExtraRange::RangeMath
5
+ end
@@ -0,0 +1,119 @@
1
+ module ExtraRange
2
+ module RangeMath
3
+ # From Ruby Facets. Returns true if this range overlaps with another range.
4
+ def overlap?(enum)
5
+ include?(enum.first) or enum.include?(first)
6
+ end
7
+
8
+ # Returns true if this range completely covers the given range.
9
+ def mask?(enum)
10
+ higher_or_equal_rhs_as?(enum) && include?(enum.first)
11
+ end
12
+
13
+ # Returns a new range containing all elements that are common to both.
14
+ def &(enum)
15
+ enum.is_a?(Enumerable) or raise ArgumentError, "value must be enumerable"
16
+ return nil unless overlap?(enum)
17
+
18
+ first_val = [first, enum.first].max
19
+ hi = lower_or_equal_rhs_as?(enum) ? self : enum
20
+
21
+ self.class.new first_val, hi.last, hi.exclude_end?
22
+ end
23
+
24
+ # Returns a new range built by merging self and the given range. If they are
25
+ # non-overlapping then we return an array of ranges.
26
+ def |(enum)
27
+ if overlap?(enum) || adjacent?(enum)
28
+ first_val = [first, enum.first].min
29
+ hi = higher_or_equal_rhs_as?(enum) ? self : enum
30
+
31
+ self.class.new first_val, hi.last, hi.exclude_end?
32
+ else
33
+ return self, enum
34
+ end
35
+ end
36
+
37
+ # Return true if this range and the other range are adjacent to each other.
38
+ # Non-sequential ranges that exclude an end can not be adjacent.
39
+ def adjacent?(enum)
40
+ adjacent_before?(enum) || enum.adjacent_before?(self)
41
+ end
42
+
43
+ # Returns a new range by subtracting the given range from self. If all
44
+ # elements are removed then returns nil. If the subtracting range results
45
+ # in a split of self then we return two ranges in a sorted array. Only works
46
+ # on ranges that represent a sequence of values and respond to succ.
47
+ def -(enum)
48
+ return self.dup unless overlap?(enum)
49
+
50
+ if enum.mask?(self)
51
+ nil
52
+ elsif first >= enum.first
53
+ ltrim enum
54
+ elsif lower_or_equal_rhs_as?(enum)
55
+ rtrim enum
56
+ else
57
+ [rtrim(enum), ltrim(enum)]
58
+ end
59
+ end
60
+
61
+ # Return the physical dimension of the range. Only works with ranges that
62
+ # represent a sequence. This can be confusing for ranges of floats, since
63
+ # (0.0..42.0) will result in the same size as (0.0...42.0). Won't work with
64
+ # ranges that don't support the minus operator.
65
+ def size
66
+ (sequential? ? one_after_max : last) - first
67
+ end
68
+
69
+ # Returns true if this range has nothing. This only happens with ranges such
70
+ # as (0...0)
71
+ def empty?
72
+ last == first && exclude_end?
73
+ end
74
+
75
+ private
76
+
77
+ # Uses the given enumerable to left trim this range
78
+ def ltrim(enum)
79
+ first_val = sequential? ? enum.one_after_max : enum.last
80
+ self.class.new first_val, last, exclude_end?
81
+ end
82
+
83
+ # Uses the given enumerable to right trim this range
84
+ def rtrim(enum)
85
+ self.class.new first, enum.first, true
86
+ end
87
+
88
+ # Return true if this range is sequential and its elements respond to succ.
89
+ def sequential?
90
+ first.respond_to?(:succ)
91
+ end
92
+
93
+ # Return true if this range has a higher or equal rhs as the given range
94
+ def higher_or_equal_rhs_as?(enum)
95
+ last > enum.last || (last == enum.last && (!exclude_end? || enum.exclude_end?))
96
+ end
97
+
98
+ # Return true if this range has a lower or equal rhs as the given range
99
+ def lower_or_equal_rhs_as?(enum)
100
+ last < enum.last || (last == enum.last && (exclude_end? || !enum.exclude_end?))
101
+ end
102
+
103
+ protected
104
+
105
+ # Return the value that is one after the max value. This allows us to compare
106
+ # ranges with our without exclude_end? set to true.
107
+ def one_after_max
108
+ exclude_end? ? last : last.succ
109
+ end
110
+
111
+ def adjacent_before?(enum)
112
+ if sequential?
113
+ one_after_max == enum.first
114
+ else
115
+ last == enum.first && !exclude_end?
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,16 @@
1
+ require 'extrarange/range_math'
2
+
3
+ module ExtraRange
4
+ class RangeWithMath < Range
5
+ include ExtraRange::RangeMath
6
+
7
+ def self.from_range(range)
8
+ new range.begin, range.end, range.exclude_end?
9
+ end
10
+
11
+ def to_range
12
+ Range.new self.begin, self.end, exclude_end?
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,198 @@
1
+ require 'extrarange/range_math'
2
+ require 'extrarange/range_with_math'
3
+
4
+ module ExtraRange
5
+ class SparseRange
6
+ include ExtraRange::RangeMath
7
+ include Enumerable
8
+
9
+ def initialize(*ranges)
10
+ @ranges = []
11
+ ranges.each { |r| self.add r }
12
+ end
13
+
14
+ def begin
15
+ @ranges.first().begin
16
+ end
17
+
18
+ alias first begin
19
+
20
+ def end
21
+ @ranges.last().end
22
+ end
23
+
24
+ alias last end
25
+
26
+ def exclude_end?
27
+ @ranges.last().exclude_end?
28
+ end
29
+
30
+ def dup
31
+ self.class.new *to_a
32
+ end
33
+
34
+ def ==(rset)
35
+ @ranges == rset.instance_variable_get(:@ranges)
36
+ end
37
+
38
+ def add(o)
39
+ case o
40
+ when Range then
41
+ _add(o)
42
+ when self.class then
43
+ o.each_range { |b| add b }
44
+ when Enumerable then
45
+ o.each { |b| add b }
46
+ else
47
+ add(o..o)
48
+ end
49
+
50
+ return self
51
+ end
52
+
53
+ alias << add
54
+
55
+ def delete(o)
56
+ case o
57
+ when Range then
58
+ _delete(o)
59
+ when self.class then
60
+ o.each_range { |s| delete s }
61
+ when Enumerable then
62
+ o.each { |s| delete s }
63
+ else
64
+ delete(o..o)
65
+ end
66
+
67
+ return self
68
+ end
69
+
70
+ def |(enum)
71
+ dup.add(enum)
72
+ end
73
+
74
+ alias + |
75
+ alias union |
76
+
77
+ def -(enum)
78
+ dup.delete(enum)
79
+ end
80
+
81
+ alias difference -
82
+
83
+ def &(o)
84
+ ret = self.class.new
85
+
86
+ case o
87
+ when Range
88
+ @ranges.each do |r|
89
+ intersection = r & o
90
+ ret.add intersection if intersection
91
+ end
92
+ ret
93
+ when self.class
94
+ o.each_range do |i|
95
+ intersection = self & i
96
+ ret.add intersection if intersection
97
+ end
98
+ ret
99
+ when Enumerable
100
+ o.each do |i|
101
+ intersection = self & i
102
+ ret.add intersection if intersection
103
+ end
104
+ ret
105
+ else
106
+ self&(o..o)
107
+ end
108
+ end
109
+
110
+ alias intersection &
111
+
112
+ # BEWARE! SparseRange is mutable!
113
+ # it would be better to change its behaviour to return new sparseranges or to
114
+ # create a separate ImmutableSparseRange class.
115
+ def hash
116
+ return @ranges.hash
117
+ end
118
+
119
+ def max(&block)
120
+ maxes = @ranges.collect {|r| r.max(&block)}
121
+ (maxes.include? nil) ? nil : maxes.max(&block)
122
+ end
123
+
124
+ def min(&block)
125
+ mins = @ranges.collect {|r| r.min(&block)}
126
+ (mins.include? nil) ? nil : mins.min(&block)
127
+ end
128
+
129
+ def include?(elem)
130
+ @ranges.any? { |r| r.include? elem }
131
+ end
132
+
133
+ alias member? include?
134
+
135
+ # currently unoptimized yet working version.
136
+ # TODO: optimize this method to use the builtin cover? method for ranges if
137
+ # they're available, i.e. we're running on Ruby 1.9.1+
138
+ alias cover? include?
139
+
140
+ def to_a
141
+ @ranges.map { |r| r.to_a }.flatten
142
+ end
143
+
144
+ def each
145
+ to_a.each{ |o| yield o }
146
+ end
147
+
148
+ def size
149
+ @ranges.inject(0) { |sum, n| sum + n.size }
150
+ end
151
+
152
+ def empty?
153
+ @ranges.empty?
154
+ end
155
+
156
+ def each_range
157
+ @ranges.map { |r| r.to_range }.each { |o| yield o }
158
+ end
159
+
160
+ def step(n=1, &block)
161
+ enumerator = to_a.select.with_index.select { |_, index| (index % n) == 0 }.collect { |x, _| x }
162
+ if block_given?
163
+ enumerator.each(&block)
164
+ else
165
+ return enumerator
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ def _add(new_range)
172
+ new_range = ExtraRange::RangeWithMath.from_range new_range
173
+ return if new_range.empty?
174
+
175
+ inserted = false
176
+
177
+ @ranges.each_with_index do |r, i|
178
+ if r.overlap?(new_range) || r.adjacent?(new_range)
179
+ new_range = new_range | r
180
+ @ranges[i] = nil
181
+ elsif r.first > new_range.first
182
+ @ranges.insert(i, new_range)
183
+ inserted = true
184
+ break
185
+ end
186
+ end
187
+
188
+ @ranges << new_range unless inserted
189
+ @ranges.compact!
190
+ end
191
+
192
+ def _delete(o)
193
+ @ranges.map! { |r| r - ExtraRange::RangeWithMath.from_range(o) }
194
+ @ranges.flatten!
195
+ @ranges.compact!
196
+ end
197
+ end
198
+ end
data/lib/extrarange.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'extrarange/sparse_range'
2
+
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: extrarange
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Alan Franzoni
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-02-18 00:00:00 Z
19
+ dependencies: []
20
+
21
+ description: The SparseRange class lets you easily collect many ranges into one.You can perform logic operations such as union and intersection on ranges with gaps. Can replace a standard range.
22
+ email:
23
+ - username@franzoni.eu
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - lib/extrarange.rb
32
+ - lib/extrarange/range_with_math.rb
33
+ - lib/extrarange/range_math.rb
34
+ - lib/extrarange/sparse_range.rb
35
+ - lib/extrarange/core_ext/range.rb
36
+ - LICENSE
37
+ - README.markdown
38
+ homepage: http://extrarange.franzoni.eu
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ hash: 3
52
+ segments:
53
+ - 0
54
+ version: "0"
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 17
61
+ segments:
62
+ - 1
63
+ - 3
64
+ - 5
65
+ version: 1.3.5
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.24
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Extended Range functionality
73
+ test_files: []
74
+