time_frame 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,20 @@
1
+ .DS_Store
2
+ *~
3
+ .#*
4
+ *.gem
5
+ *.rbc
6
+ .bundle
7
+ .config
8
+ .yardoc
9
+ *.swp
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
@@ -0,0 +1,12 @@
1
+ AllCops:
2
+ Exclude: []
3
+
4
+ # Setting the line length to a maximum of 80 chars.
5
+ LineLength:
6
+ Enabled: true
7
+ Max: 80
8
+
9
+ # Disabling the regex checks for now, since it fails with some generated gemspec
10
+ # lines.
11
+ RegexpLiteral:
12
+ Enabled: false
@@ -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
@@ -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!
@@ -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.
@@ -0,0 +1,9 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rubocop/rake_task'
3
+ require 'bundler/gem_tasks'
4
+
5
+ Rubocop::RakeTask.new
6
+
7
+ RSpec::Core::RakeTask.new :spec
8
+
9
+ task default: [:spec, :rubocop]
@@ -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