range_with_gaps 0.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/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
+