range_with_gaps 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Timothy Elliott
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.
@@ -0,0 +1,94 @@
1
+ ## Range With Gaps: A ruby class to work with discontinuous ranges
2
+
3
+ The RangeWithGaps class lets you easily collect many ranges into one. It accepts
4
+ individual values (3, 4, 5) as well as ranges (2..3000) and will fold them
5
+ together to efficiently work with them as a single set of ranges.
6
+
7
+ Once you have inserted your values and ranges, you can perform logic operations
8
+ such as union and intersection. This is particularly useful for finding
9
+ overlapping time spans.
10
+
11
+ I took care to allow for non-sequential ranges, such as ranges of floats, to
12
+ work with this class. Non-sequential ranges are ranges for data types that don't
13
+ have a succ method.
14
+
15
+ This gem adds only a single constant to the Ruby namespace; RangeWithGaps.
16
+
17
+ ## Usage
18
+
19
+ In order to include the class
20
+
21
+ require 'range_with_gaps'
22
+
23
+ Optionally, you can add math operations to the Range class:
24
+
25
+ require 'range_with_gaps/core_ext/range'
26
+
27
+ Initialize a range with gaps, the order doesn't matter, and overlapping values
28
+ are folded together:
29
+
30
+ range_with_gaps = RangeWithGaps.new 55...900, 1..34, 22, 23
31
+
32
+ Add values to a range with gaps:
33
+
34
+ range_with_gaps << 0.4
35
+ range_with_gaps.add(Time.now...(Time.now + 5000))
36
+
37
+ Delete values from a range with gaps:
38
+
39
+ range_with_gaps.delete(Date.today - 15)
40
+ range_with_gaps.delete(50..3000)
41
+
42
+ Fast calculation of the size of a range with gaps:
43
+
44
+ range_with_gaps.size
45
+
46
+ Use logic operations on a range with gaps:
47
+
48
+ # returns a range with gaps of all overlapping ranges
49
+ range_with_gaps_one & range_with_gaps_two
50
+ range_with_gaps & ((Date.today - 30)..Date.today)
51
+
52
+ # returns a range with gaps of all ranges combined
53
+ range_with_gaps_one | range_with_gaps_two
54
+
55
+ I created this class in order to simplify the calculation of billing periods in
56
+ a rails project that uses a audit table:
57
+
58
+ require 'range_with_gaps'
59
+
60
+ def get_premium_time_spans
61
+ premium_time_spans = RangeWithGaps.new
62
+
63
+ # Get all events from an audit table, i.e. every time a customer toggles
64
+ # the premium flag on their account.
65
+ premium_toggles = Customer.audits.premium_column
66
+
67
+ # We only care when premium was on, so eat the first event if it was a
68
+ # toggle to a non-premium account.
69
+ premium_toggles.shift unless premium_toggles.first.value
70
+
71
+ premium_toggles.each_slice(2) do |toggle_pair|
72
+ # If we only got one element in the slice then inject a fake toggle
73
+ # at the end of the span
74
+ span_end = toggle_pair[1] ? toggle_pair[1].created_at : Time.now
75
+
76
+ premium_time_spans.add(toggle_pair[0].created_at..span_end)
77
+ end
78
+
79
+ premium_time_spans
80
+ end
81
+
82
+ premium_time_spans = get_premium_time_spans
83
+
84
+ # Calculate how long the customer had a premium account in January
85
+ january = Time.local(2010, 1, 1)...Time.local(2010, 2, 1)
86
+ puts "January Premium: #{(premium_time_spans & january).size} seconds"
87
+
88
+ # Calculate how long the customer had a premium account during the two last
89
+ # service outages
90
+ disk_recovery = Time.local(2009, 12, 24, 15, 30)..Time.local(2009, 12, 24, 16)
91
+ net_outage = Time.local(2010, 2, 1, 12, 15)..Time.local(2010, 2, 2, 2, 30)
92
+ outages = RangeWithGaps.new disk_recovery, net_outage
93
+
94
+ puts "Outage Premium: #{(premium_time_spans & outages).size} seconds"
@@ -0,0 +1,122 @@
1
+ require 'range_with_gaps/range_with_math'
2
+
3
+ class RangeWithGaps
4
+ def initialize(*ranges)
5
+ @ranges = []
6
+ ranges.each{|r| self.add r}
7
+ end
8
+
9
+ def dup
10
+ self.class.new *to_a
11
+ end
12
+
13
+ def ==(rset)
14
+ @ranges == rset.instance_variable_get(:@ranges)
15
+ end
16
+
17
+ def add(o)
18
+ case o
19
+ when Range then _add(o)
20
+ when self.class, Enumerable then o.each{|b| add b}
21
+ else add(o..o)
22
+ end
23
+
24
+ return self
25
+ end
26
+ alias << add
27
+
28
+ def delete(o)
29
+ case o
30
+ when Range then _delete(o)
31
+ when self.class, Enumerable then o.each{|s| delete s}
32
+ else delete(o..o)
33
+ end
34
+
35
+ return self
36
+ end
37
+
38
+ def |(enum)
39
+ dup.add(enum)
40
+ end
41
+ alias + |
42
+ alias union |
43
+
44
+ def -(enum)
45
+ dup.delete(enum)
46
+ end
47
+ alias difference -
48
+
49
+ def &(o)
50
+ ret = self.class.new
51
+
52
+ case o
53
+ when Range
54
+ @ranges.each do |r|
55
+ intersection = r & o
56
+ ret.add intersection if intersection
57
+ end
58
+ ret
59
+ when self.class, Enumerable
60
+ o.each do |i|
61
+ intersection = self & i
62
+ ret.add intersection if intersection
63
+ end
64
+ ret
65
+ else self&(o..o)
66
+ end
67
+ end
68
+ alias intersection &
69
+
70
+ def include?(elem)
71
+ @ranges.any?{|r| r.include? elem}
72
+ end
73
+
74
+ def to_a
75
+ @ranges.map{ |r| r.to_range }
76
+ end
77
+
78
+ def each
79
+ to_a.each{|o| yield o}
80
+ end
81
+
82
+ def size
83
+ @ranges.inject(0){|sum, n| sum + n.size}
84
+ end
85
+
86
+ def entries
87
+ @ranges.map{|r| r.to_a}.flatten
88
+ end
89
+
90
+ def empty?
91
+ @ranges.empty?
92
+ end
93
+
94
+ private
95
+
96
+ def _add(new_range)
97
+ new_range = RangeWithMath.from_range new_range
98
+ return if new_range.empty?
99
+
100
+ inserted = false
101
+
102
+ @ranges.each_with_index do |r, i|
103
+ if r.overlap?(new_range) || r.adjacent?(new_range)
104
+ new_range = new_range | r
105
+ @ranges[i] = nil
106
+ elsif r.first > new_range.first
107
+ @ranges.insert(i, new_range)
108
+ inserted = true
109
+ break
110
+ end
111
+ end
112
+
113
+ @ranges << new_range unless inserted
114
+ @ranges.compact!
115
+ end
116
+
117
+ def _delete(o)
118
+ @ranges.map!{|r| r - o}
119
+ @ranges.flatten!
120
+ @ranges.compact!
121
+ end
122
+ end
@@ -0,0 +1,5 @@
1
+ require 'range_with_gaps/range_math'
2
+
3
+ class Range
4
+ include RangeWithGaps::RangeMath
5
+ end
@@ -0,0 +1,119 @@
1
+ class RangeWithGaps
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,15 @@
1
+ require 'range_with_gaps/range_math'
2
+
3
+ class RangeWithGaps
4
+ class RangeWithMath < Range
5
+ include 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
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: range_with_gaps
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Timothy Elliott
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-18 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: The RangeWithGaps class lets you easily collect many ranges into one.You can perform logic operations such as union and intersection on ranges with gaps.
17
+ email:
18
+ - tle@holymonkey.com
19
+ executables: []
20
+
21
+ extensions: []
22
+
23
+ extra_rdoc_files: []
24
+
25
+ files:
26
+ - lib/range_with_gaps.rb
27
+ - lib/range_with_gaps/range_with_math.rb
28
+ - lib/range_with_gaps/range_math.rb
29
+ - lib/range_with_gaps/core_ext/range.rb
30
+ - LICENSE
31
+ - README.markdown
32
+ has_rdoc: true
33
+ homepage: http://github.com/ender672/range_with_gaps
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 1.3.5
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.3.5
57
+ signing_key:
58
+ specification_version: 3
59
+ summary: Like Ranges, but with gaps.
60
+ test_files: []
61
+