time_frame 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +12 -0
- data/.ruby-version +1 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +84 -0
- data/README.md +120 -0
- data/Rakefile +9 -0
- data/lib/time_frame.rb +11 -0
- data/lib/time_frame/time_frame.rb +124 -0
- data/lib/time_frame/time_frame_covered.rb +15 -0
- data/lib/time_frame/time_frame_overlaps.rb +47 -0
- data/lib/time_frame/time_frame_splitter.rb +17 -0
- data/lib/time_frame/time_frame_uniter.rb +23 -0
- data/lib/time_frame/version.rb +4 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/time_range_spec.rb +1075 -0
- data/time_frame.gemspec +29 -0
- metadata +81 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 45120e4f1dda9a59b63972c578060e6e94863907
|
4
|
+
data.tar.gz: 60bea2d8848eeb0fd283bd8b7d484341944cae63
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 40e9e0a39e8a81cfe6b0c31c7d0124c7f5e8eb99a741af5d1ab1dd808e123b91afdac459801fa8de1ceb69c5c88f8e7eeea0b12a74f84e100510a0de8167914d
|
7
|
+
data.tar.gz: 270624b7fdde20d0f3a37cc4be3b72a4189313554fc00d851fa56f0394d675849c2d60c0817a0ccdb4cddf89cd8f0f70c8c796f4dc59c10caf4e05e1728aaa9e
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.1.2
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in plan-logic.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'bundler'
|
7
|
+
gem 'rake'
|
8
|
+
|
9
|
+
group :development, :test do
|
10
|
+
gem 'rubocop'
|
11
|
+
gem 'geminabox'
|
12
|
+
end
|
13
|
+
|
14
|
+
group :test do
|
15
|
+
gem 'simplecov'
|
16
|
+
gem 'rspec'
|
17
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
time_frame (0.0.0)
|
5
|
+
activesupport (~> 4.0)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
activesupport (4.1.1)
|
11
|
+
i18n (~> 0.6, >= 0.6.9)
|
12
|
+
json (~> 1.7, >= 1.7.7)
|
13
|
+
minitest (~> 5.1)
|
14
|
+
thread_safe (~> 0.1)
|
15
|
+
tzinfo (~> 1.1)
|
16
|
+
ast (2.0.0)
|
17
|
+
builder (3.2.2)
|
18
|
+
diff-lcs (1.2.5)
|
19
|
+
docile (1.1.3)
|
20
|
+
faraday (0.9.0)
|
21
|
+
multipart-post (>= 1.2, < 3)
|
22
|
+
geminabox (0.12.4)
|
23
|
+
builder
|
24
|
+
faraday
|
25
|
+
httpclient (>= 2.2.7)
|
26
|
+
nesty
|
27
|
+
sinatra (>= 1.2.7)
|
28
|
+
httpclient (2.3.4.1)
|
29
|
+
i18n (0.6.9)
|
30
|
+
json (1.8.1)
|
31
|
+
minitest (5.3.3)
|
32
|
+
multi_json (1.10.0)
|
33
|
+
multipart-post (2.0.0)
|
34
|
+
nesty (1.0.2)
|
35
|
+
parser (2.1.9)
|
36
|
+
ast (>= 1.1, < 3.0)
|
37
|
+
slop (~> 3.4, >= 3.4.5)
|
38
|
+
powerpack (0.0.9)
|
39
|
+
rack (1.5.2)
|
40
|
+
rack-protection (1.5.3)
|
41
|
+
rack
|
42
|
+
rainbow (2.0.0)
|
43
|
+
rake (10.3.1)
|
44
|
+
rspec (2.14.1)
|
45
|
+
rspec-core (~> 2.14.0)
|
46
|
+
rspec-expectations (~> 2.14.0)
|
47
|
+
rspec-mocks (~> 2.14.0)
|
48
|
+
rspec-core (2.14.8)
|
49
|
+
rspec-expectations (2.14.5)
|
50
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
51
|
+
rspec-mocks (2.14.6)
|
52
|
+
rubocop (0.21.0)
|
53
|
+
json (>= 1.7.7, < 2)
|
54
|
+
parser (~> 2.1.9)
|
55
|
+
powerpack (~> 0.0.6)
|
56
|
+
rainbow (>= 1.99.1, < 3.0)
|
57
|
+
ruby-progressbar (~> 1.4)
|
58
|
+
ruby-progressbar (1.5.0)
|
59
|
+
simplecov (0.8.2)
|
60
|
+
docile (~> 1.1.0)
|
61
|
+
multi_json
|
62
|
+
simplecov-html (~> 0.8.0)
|
63
|
+
simplecov-html (0.8.0)
|
64
|
+
sinatra (1.4.5)
|
65
|
+
rack (~> 1.4)
|
66
|
+
rack-protection (~> 1.4)
|
67
|
+
tilt (~> 1.3, >= 1.3.4)
|
68
|
+
slop (3.5.0)
|
69
|
+
thread_safe (0.3.3)
|
70
|
+
tilt (1.4.1)
|
71
|
+
tzinfo (1.1.0)
|
72
|
+
thread_safe (~> 0.1)
|
73
|
+
|
74
|
+
PLATFORMS
|
75
|
+
ruby
|
76
|
+
|
77
|
+
DEPENDENCIES
|
78
|
+
bundler
|
79
|
+
geminabox
|
80
|
+
rake
|
81
|
+
rspec
|
82
|
+
rubocop
|
83
|
+
simplecov
|
84
|
+
time_frame!
|
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
# TimeFrame
|
2
|
+
|
3
|
+
## Description
|
4
|
+
|
5
|
+
The time frame class provides an specialized and enhanced frame for time values.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
`gem install time_frame`
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
You can create a `TimeFrame` instance by specifying `min` and `max`
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
time_frame = TimeFrame.new(min: Time.now, max: Time.now + 1.day)
|
17
|
+
```
|
18
|
+
|
19
|
+
or just by specifying a `min` and `duration`
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
time_frame = TimeFrame.new(min: Time.now, duration: 1.day)
|
23
|
+
```
|
24
|
+
|
25
|
+
Let's play around a bit:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
# Create a time frame instance from today with duration of 1 day
|
29
|
+
time_frame = TimeFrame.new(min: Time.now, duration: 1.day)
|
30
|
+
# => 2014-05-07 14:58:47 +0200..2014-05-08 14:58:47 +0200
|
31
|
+
|
32
|
+
# Get the duration
|
33
|
+
time_frame.duration
|
34
|
+
# => 86400.0 seconds
|
35
|
+
|
36
|
+
# Shift the whole time frame by... let's say... 2 days!
|
37
|
+
later = time_frame.shift_by(2.days)
|
38
|
+
# => 2014-05-09 14:58:47 +0200..2014-05-10 14:58:47 +0200
|
39
|
+
|
40
|
+
# Shifting can also be done in the other direction...
|
41
|
+
earlier = time_frame.shift_by(-2.days)
|
42
|
+
# => 2014-05-05 14:58:47 +0200..2014-05-06 14:58:47 +0200
|
43
|
+
|
44
|
+
# Is another time covered by our time frame?
|
45
|
+
my_time = Time.new(2014, 5, 7, 16)
|
46
|
+
time_frame.cover?(my_time)
|
47
|
+
# => true
|
48
|
+
|
49
|
+
# Deviation to another time?
|
50
|
+
earlier_time = time_frame.min - 1.day
|
51
|
+
later_time = time_frame.max + 4.days
|
52
|
+
time_frame.deviation_of(earlier_time)
|
53
|
+
# => -86400.0
|
54
|
+
time_frame.deviation_of(later_time)
|
55
|
+
# => 345600.0
|
56
|
+
# No deviation expected here:
|
57
|
+
time_frame.deviation_of(time_frame.min + 20.minutes)
|
58
|
+
# => 0
|
59
|
+
# ... yay!
|
60
|
+
|
61
|
+
# Shifting to another time... duration remains:
|
62
|
+
time_frame.shift_to(Time.new(2016, 1, 1))
|
63
|
+
# => 2016-01-01 00:00:00 +0100..2016-01-02 00:00:00 +0100
|
64
|
+
|
65
|
+
# Checking whether another time frame overlaps:
|
66
|
+
other_frame = TimeFrame.new(
|
67
|
+
min: time_frame.min - 3.days,
|
68
|
+
max: time_frame.min + 40.minutes
|
69
|
+
)
|
70
|
+
time_frame.overlaps?(other_frame)
|
71
|
+
# => true
|
72
|
+
|
73
|
+
# Time frame without another time frame:
|
74
|
+
time_frame = TimeFrame.new(min: Time.new(2014, 5, 12), duration: 1.day)
|
75
|
+
# => 2014-05-12 00:00:00 +0200..2014-05-13 00:00:00 +0200
|
76
|
+
other = TimeFrame.new(min: Time.new(2014, 5, 12, 19), duration: 10.minutes)
|
77
|
+
# => 2014-05-12 19:00:00 +0200..2014-05-12 19:10:00 +0200
|
78
|
+
time_frame.without(other)
|
79
|
+
# => [2014-05-12 00:00:00 +0200..2014-05-12 19:00:00 +0200, 2014-05-12 19:10:00 +0200..2014-05-13 00:00:00 +0200]
|
80
|
+
another = other.shift_by(15.minutes)
|
81
|
+
# => 2014-05-12 19:15:00 +0200..2014-05-12 19:25:00 +0200
|
82
|
+
# You can also use an array for substraction:
|
83
|
+
time_frame.without(*[other, another])
|
84
|
+
# => [2014-05-12 00:00:00 +0200..2014-05-12 19:00:00 +0200, 2014-05-12 19:10:00 +0200..2014-05-12 19:15:00 +0200, 2014-05-12 19:25:00 +0200..2014-05-13 00:00:00 +0200]
|
85
|
+
|
86
|
+
# Use of the mathematical &. The intersection is returned:
|
87
|
+
time_frame = TimeFrame.new(min: Time.new(2014), duration: 1.day)
|
88
|
+
# => 2014-01-01 00:00:00 +0100..2014-01-02 00:00:00 +0100
|
89
|
+
other_time_frame = time_frame.shift_by(12.hours)
|
90
|
+
# => 2014-01-01 12:00:00 +0100..2014-01-02 12:00:00 +0100
|
91
|
+
time_frame & other_time_frame
|
92
|
+
# => 2014-01-01 12:00:00 +0100..2014-01-02 00:00:00 +0100
|
93
|
+
|
94
|
+
```
|
95
|
+
|
96
|
+
These are the most common functionalities of the `TimeFrame` class, but there is quite more to discover. If you have an array of time frames, you can compute their union and pairwise intersection using `TimeFrame.union` and `TimeFrame.intersection`. For two sorted arrays of time frames, you can traverse all overlaps of time frames in the first array with time frames in the second array in **linear time** using `TimeFrame.each_overlap`.
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
|
100
|
+
# each_overlap in a real life sample:
|
101
|
+
|
102
|
+
husband_at_home = [
|
103
|
+
TimeFrame.new(min: Time.new(2014, 2, 1), duration: 2.days),
|
104
|
+
TimeFrame.new(min: Time.new(2014, 5, 1), duration: 4.days),
|
105
|
+
TimeFrame.new(min: Time.new(2014, 7, 1), duration: 10.days)
|
106
|
+
]
|
107
|
+
|
108
|
+
mother_in_law_visits = [
|
109
|
+
TimeFrame.new(min: Time.new(2014, 2, 2), duration: 1.days),
|
110
|
+
TimeFrame.new(min: Time.new(2014, 7, 3), duration: 2.days)
|
111
|
+
]
|
112
|
+
|
113
|
+
TimeFrame.each_overlap(mother_in_law_visits, husband_at_home) do |overlap|
|
114
|
+
puts "Houston... we have a problem... from #{overlap.min} to #{overlap.max}"
|
115
|
+
end
|
116
|
+
|
117
|
+
```
|
118
|
+
|
119
|
+
## Does `TimeFrame` inherit from `Range`?
|
120
|
+
No. Ruby's `Range` class is multi-purpose, it can hold contiuous values (like floats), as well as discrete values (like integers) and behaves differently according to their type. Instance methods like `#each` or `#size` just don't make sense for time values, the same is true for all methods provided by the `Enumerable` mixin.
|
data/Rakefile
ADDED
data/lib/time_frame.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
|
3
|
+
# avoid i18n deprecation warning
|
4
|
+
I18n.enforce_available_locales = false
|
5
|
+
|
6
|
+
require 'time_frame/time_frame_splitter'
|
7
|
+
require 'time_frame/time_frame_covered'
|
8
|
+
require 'time_frame/time_frame_overlaps'
|
9
|
+
require 'time_frame/time_frame_uniter'
|
10
|
+
|
11
|
+
require 'time_frame/time_frame'
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# class that models a time frame
|
2
|
+
class TimeFrame
|
3
|
+
include Splitter
|
4
|
+
attr_reader :min, :max
|
5
|
+
|
6
|
+
def initialize(args)
|
7
|
+
min = args.fetch(:min)
|
8
|
+
max = args.fetch(:max, nil) || min + args.fetch(:duration)
|
9
|
+
check_bounds(max, min)
|
10
|
+
@max = max
|
11
|
+
@min = min
|
12
|
+
end
|
13
|
+
|
14
|
+
def duration
|
15
|
+
(max - min).seconds
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
min == other.min &&
|
20
|
+
max == other.max
|
21
|
+
end
|
22
|
+
|
23
|
+
def cover?(element)
|
24
|
+
if element.respond_to?(:min) && element.respond_to?(:max)
|
25
|
+
return min <= element.min && element.max <= max
|
26
|
+
end
|
27
|
+
min <= element && element <= max
|
28
|
+
end
|
29
|
+
|
30
|
+
def deviation_of(item)
|
31
|
+
case
|
32
|
+
when item.respond_to?(:min) && item.respond_to?(:max)
|
33
|
+
[deviation_of(item.min), deviation_of(item.max)].min_by { |a| a.abs }
|
34
|
+
when cover?(item)
|
35
|
+
0
|
36
|
+
when item < min
|
37
|
+
item - min
|
38
|
+
else
|
39
|
+
item - max
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def empty?
|
44
|
+
min == max
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.union(time_frames, options = {})
|
48
|
+
Uniter.new(time_frames, options).unite
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.intersection(time_frames)
|
52
|
+
time_frames.reduce(time_frames.first) do |intersection, time_frame|
|
53
|
+
return unless intersection
|
54
|
+
intersection & time_frame
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns true if the interior intersect.
|
59
|
+
def overlaps?(other)
|
60
|
+
other.max > min && other.min < max
|
61
|
+
end
|
62
|
+
|
63
|
+
def &(other)
|
64
|
+
new_min = [min, other.min].max
|
65
|
+
new_max = [max, other.max].min
|
66
|
+
TimeFrame.new(min: new_min, max: new_max) if new_min <= new_max
|
67
|
+
end
|
68
|
+
|
69
|
+
def shift_by(duration)
|
70
|
+
TimeFrame.new(min: @min + duration, duration: self.duration)
|
71
|
+
end
|
72
|
+
|
73
|
+
def shift_to(time)
|
74
|
+
TimeFrame.new(min: time, duration: duration)
|
75
|
+
end
|
76
|
+
|
77
|
+
def without(*args)
|
78
|
+
frames = args.select { |frame| overlaps?(frame) }
|
79
|
+
frames = TimeFrame.union(frames)
|
80
|
+
|
81
|
+
frames.reduce([self]) do |result, frame_to_exclude|
|
82
|
+
last_frame = result.pop
|
83
|
+
result + last_frame.without_frame(frame_to_exclude)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.covering_time_frame_for(time_frames)
|
88
|
+
CoveredFrame.new(time_frames).frame
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.each_overlap(frames1, frames2)
|
92
|
+
Overlaps.new(frames1, frames2).each do |first, second|
|
93
|
+
yield(first, second)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def inspect
|
98
|
+
"#{min}..#{max}"
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def without_frame(other)
|
104
|
+
intersection = self & other
|
105
|
+
# this case is never used up to now (15.03.2013),
|
106
|
+
# since without selects the values correctly
|
107
|
+
return [self] unless intersection
|
108
|
+
|
109
|
+
result = []
|
110
|
+
if intersection.min > min
|
111
|
+
result << TimeFrame.new(min: min, max: intersection.min)
|
112
|
+
end
|
113
|
+
if intersection.max < max
|
114
|
+
result << TimeFrame.new(min: intersection.max, max: max)
|
115
|
+
end
|
116
|
+
result
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def check_bounds(max, min)
|
122
|
+
fail ArgumentError, 'min is greater than max.' if min > max
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class TimeFrame
|
2
|
+
# Getting the covering time frame from a bunch of time_frame's.
|
3
|
+
class CoveredFrame
|
4
|
+
def initialize(time_frames)
|
5
|
+
@time_frames = time_frames
|
6
|
+
end
|
7
|
+
|
8
|
+
def frame
|
9
|
+
return nil unless @time_frames.any?
|
10
|
+
min = @time_frames.min_by(&:min).min
|
11
|
+
max = @time_frames.max_by(&:max).max
|
12
|
+
TimeFrame.new(min: min, max: max)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class TimeFrame
|
2
|
+
# Traverses all intersections of in the cross product of two arrays of
|
3
|
+
# time_frames and yields the block for each pair (linear runtime)
|
4
|
+
#
|
5
|
+
# NOTE:
|
6
|
+
# * requires each of the arrays to consist of pairwise disjoint elements
|
7
|
+
# * requires each of the arrays to be sorted
|
8
|
+
class Overlaps
|
9
|
+
def initialize(array1, array2)
|
10
|
+
@array1 = array1.dup
|
11
|
+
@array2 = array2.dup
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
return [] if @array1.empty? || @array2.empty?
|
16
|
+
yield_current_items(&block) if current_items_overlapping?
|
17
|
+
while arrays_have_many_items?
|
18
|
+
shift
|
19
|
+
yield_current_items(&block) if current_items_overlapping?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def shift
|
26
|
+
if @array2.one? ||
|
27
|
+
@array1.many? && @array1.second.min < @array2.second.min
|
28
|
+
@array1.shift
|
29
|
+
else
|
30
|
+
@array2.shift
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def arrays_have_many_items?
|
35
|
+
@array1.many? || @array2.many?
|
36
|
+
end
|
37
|
+
|
38
|
+
def yield_current_items
|
39
|
+
yield @array1.first, @array2.first
|
40
|
+
end
|
41
|
+
|
42
|
+
def current_items_overlapping?
|
43
|
+
time_frame = @array1.first & @array2.first
|
44
|
+
time_frame && time_frame.duration > 0
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|