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 +20 -0
- data/README.markdown +94 -0
- data/lib/range_with_gaps.rb +122 -0
- data/lib/range_with_gaps/core_ext/range.rb +5 -0
- data/lib/range_with_gaps/range_math.rb +119 -0
- data/lib/range_with_gaps/range_with_math.rb +15 -0
- metadata +61 -0
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.
|
data/README.markdown
ADDED
@@ -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,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
|
+
|