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 +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
|
+
|