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.
@@ -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