acts_as_flux_capacitor 0.6.3 → 0.6.4
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/Manifest.txt +6 -2
- data/config/hoe.rb +1 -1
- data/lib/acts_as_flux_capacitor/base.rb +129 -0
- data/lib/acts_as_flux_capacitor/compare_events.rb +29 -0
- data/lib/acts_as_flux_capacitor/core_class_extensions.rb +42 -0
- data/lib/acts_as_flux_capacitor/operate_on_durations.rb +23 -0
- data/lib/acts_as_flux_capacitor/operate_on_events.rb +60 -0
- data/lib/acts_as_flux_capacitor/sql.rb +81 -0
- data/lib/acts_as_flux_capacitor/validation.rb +13 -0
- data/lib/acts_as_flux_capacitor/version.rb +1 -1
- data/lib/acts_as_flux_capacitor.rb +10 -11
- data/spec/report.html +7 -2
- data/website/index.html +1 -1
- metadata +20 -7
- data/lib/acts_as_flux_capacitor/acts_as_flux_capacitor.rb +0 -341
- data/lib/acts_as_flux_capacitor/range_overlap.rb +0 -42
data/Manifest.txt
CHANGED
@@ -7,9 +7,13 @@ Rakefile
|
|
7
7
|
config/hoe.rb
|
8
8
|
config/requirements.rb
|
9
9
|
lib/acts_as_flux_capacitor.rb
|
10
|
-
lib/acts_as_flux_capacitor/
|
10
|
+
lib/acts_as_flux_capacitor/base.rb
|
11
|
+
lib/acts_as_flux_capacitor/sql.rb
|
12
|
+
lib/acts_as_flux_capacitor/compare_events.rb
|
13
|
+
lib/acts_as_flux_capacitor/operate_on_events.rb
|
14
|
+
lib/acts_as_flux_capacitor/operate_on_durations.rb
|
11
15
|
lib/acts_as_flux_capacitor/core_class_extensions.rb
|
12
|
-
lib/acts_as_flux_capacitor/
|
16
|
+
lib/acts_as_flux_capacitor/validation.rb
|
13
17
|
lib/acts_as_flux_capacitor/temporal.rb
|
14
18
|
lib/acts_as_flux_capacitor/version.rb
|
15
19
|
script/console
|
data/config/hoe.rb
CHANGED
@@ -8,7 +8,7 @@ RUBYFORGE_PROJECT = 'aafc' # The unix name for your project
|
|
8
8
|
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
9
9
|
DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
|
10
10
|
EXTRA_DEPENDENCIES = [
|
11
|
-
|
11
|
+
['activerecord' , '>= 1.2.0']
|
12
12
|
] # An array of rubygem dependencies [name, version]
|
13
13
|
|
14
14
|
@config_file = "~/.rubyforge/user-config.yml"
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module HolmesLabs::Acts::FluxCapacitor::ActiveRecord #:nodoc:
|
2
|
+
|
3
|
+
def self.included(base) # :nodoc:
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def acts_as_flux_capacitor
|
9
|
+
unless acts_as_flux_capacitor?
|
10
|
+
class << self
|
11
|
+
alias_method :original_find, :find
|
12
|
+
end
|
13
|
+
end
|
14
|
+
include Temporal
|
15
|
+
include InstanceMethods
|
16
|
+
include OperateOnEvents
|
17
|
+
include OperateOnDurations
|
18
|
+
include CompareEvents
|
19
|
+
include Validation
|
20
|
+
end
|
21
|
+
|
22
|
+
def flux_capacitor?
|
23
|
+
included_modules.include? InstanceMethods
|
24
|
+
end
|
25
|
+
alias acts_as_flux_capacitor? flux_capacitor?
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
module InstanceMethods #:nodoc:
|
31
|
+
def self.included(base) # :nodoc:
|
32
|
+
base.extend ClassMethods
|
33
|
+
base.extend QueryGenerators
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
|
38
|
+
CUSTOM_FIND_OPTIONS = [
|
39
|
+
:before, # raw sql
|
40
|
+
:after, # raw sql
|
41
|
+
|
42
|
+
:now,
|
43
|
+
:at,
|
44
|
+
|
45
|
+
:past,
|
46
|
+
:present,
|
47
|
+
:future,
|
48
|
+
|
49
|
+
:from,
|
50
|
+
:to,
|
51
|
+
|
52
|
+
:starts_before, # raw sql
|
53
|
+
:ends_after # raw sql
|
54
|
+
]
|
55
|
+
|
56
|
+
def find(*args)
|
57
|
+
options = args.extract_options!
|
58
|
+
|
59
|
+
current_time = options.delete(:current_time) || Time.now
|
60
|
+
temporal_conditions = extract_temporal_conditions!(options)
|
61
|
+
sql_temporal = sql_generate_temporal(temporal_conditions,current_time)
|
62
|
+
|
63
|
+
with_scope( :find => {
|
64
|
+
:conditions => sql_temporal ,
|
65
|
+
:order => sql_oldest_first }) do
|
66
|
+
original_find(*(args << options))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def overlap(an_event,another_event)
|
71
|
+
an_event.to_range.overlap(another_event.to_range)
|
72
|
+
end
|
73
|
+
|
74
|
+
def overlap?(an_event,another_event)
|
75
|
+
an_event.to_range.overlap?(another_event.to_range)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def extract_temporal_conditions!(options)
|
81
|
+
temporal_conditions = {}
|
82
|
+
|
83
|
+
options.each do |key,val|
|
84
|
+
temporal_conditions.merge!({key, options.delete(key)}) if CUSTOM_FIND_OPTIONS.include?(key)
|
85
|
+
end
|
86
|
+
|
87
|
+
temporal_conditions
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_range
|
92
|
+
(start..finish)
|
93
|
+
end
|
94
|
+
alias range to_range
|
95
|
+
|
96
|
+
def start ; read_attribute(:begins_at) ; end
|
97
|
+
def start=(val) ; write_attribute(:begins_at,val); end
|
98
|
+
|
99
|
+
def finish ; read_attribute(:ends_at) ; end
|
100
|
+
def finish=(val) ; write_attribute(:ends_at,val) ; end
|
101
|
+
|
102
|
+
alias start_time start
|
103
|
+
alias start_time= start=
|
104
|
+
alias beginning start
|
105
|
+
alias beginning= start=
|
106
|
+
alias end_time finish
|
107
|
+
alias end_time= finish=
|
108
|
+
alias from start
|
109
|
+
alias from= start=
|
110
|
+
alias to finish
|
111
|
+
alias to= finish=
|
112
|
+
alias ending finish
|
113
|
+
alias ending= finish=
|
114
|
+
end # InstanceMethods
|
115
|
+
end
|
116
|
+
|
117
|
+
ActiveRecord::Base.send :include, HolmesLabs::Acts::FluxCapacitor::ActiveRecord
|
118
|
+
|
119
|
+
# TODO
|
120
|
+
###########
|
121
|
+
# allow events to be created by start time and duration
|
122
|
+
# force exclusivity of events at any given time via parameter to acts_as_event()
|
123
|
+
# add new validations
|
124
|
+
# rake tasks
|
125
|
+
|
126
|
+
|
127
|
+
# NOTES
|
128
|
+
###########
|
129
|
+
# verify #find_* returns ordered results unless specified otherwise.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HolmesLabs::Acts::FluxCapacitor::ActiveRecord::CompareEvents
|
2
|
+
def overlaps_with?(other_event)
|
3
|
+
self.to_range.overlaps?(other_event.to_range)
|
4
|
+
end
|
5
|
+
|
6
|
+
def overlap_with(other_event)
|
7
|
+
(self.to_range.overlap(other_event.to_range)).size
|
8
|
+
end
|
9
|
+
alias overlap overlap_with
|
10
|
+
|
11
|
+
def back_to_back_with?(other_event,tolerance = 2.seconds)
|
12
|
+
# tolerance is in seconds.
|
13
|
+
self.start_time.before?(other_event.start_time) ? first_to_start = self : first_to_start = other_event
|
14
|
+
first_to_start.ends_at.approx_eql?(other_event.begins_at,tolerance)
|
15
|
+
end
|
16
|
+
|
17
|
+
def duration
|
18
|
+
self.class.length_of_time_until(self.ending,self.beginning)
|
19
|
+
end
|
20
|
+
alias length_of_time duration
|
21
|
+
alias length duration
|
22
|
+
|
23
|
+
def duration=(new_duration)
|
24
|
+
self.ending = self.beginning + new_duration
|
25
|
+
end
|
26
|
+
alias length_of_time= duration=
|
27
|
+
alias length= duration=
|
28
|
+
end
|
29
|
+
|
@@ -16,3 +16,45 @@ class Time
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
class Range
|
20
|
+
|
21
|
+
# the overlaps?(range) method comes from ActiveSupport::CoreExtensions::Range
|
22
|
+
# thanks to Nick Ang for suggesting its use.
|
23
|
+
# most of its tests were written by Jack Christensen of
|
24
|
+
# http://www.jackchristensen.com/article/2/detecting-overlapping-ranges
|
25
|
+
|
26
|
+
# Returns true if ranges overlap, false otherwise
|
27
|
+
def overlaps? other
|
28
|
+
include?(other.first) || other.include?(first)
|
29
|
+
end
|
30
|
+
|
31
|
+
# all of this added by jim cropcho:
|
32
|
+
|
33
|
+
def overlap(other_range)
|
34
|
+
return 0 unless self.overlaps?(other_range)
|
35
|
+
([self.first,other_range.first].max)..([self.last,other_range.last].min)
|
36
|
+
end
|
37
|
+
|
38
|
+
def size
|
39
|
+
if self.first.is_a?(Time) && self.last.is_a?(Time)
|
40
|
+
time_size
|
41
|
+
elsif self.first.is_a?(Fixnum) || self.first.is_a?(Float) &&
|
42
|
+
self.last.is_a?(Fixnum) || self.last.is_a?(Float)
|
43
|
+
number_size
|
44
|
+
else
|
45
|
+
raise "size cannot handle these object types."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
alias length size
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def time_size
|
53
|
+
raise "both endpoints of Range must be Time objects." unless self.last.is_a?(Time) && self.first.is_a?(Time)
|
54
|
+
return self.last-self.first
|
55
|
+
end
|
56
|
+
|
57
|
+
def number_size # technically, off by infinity^(-1) when either or both endpoints are excluded. meh.
|
58
|
+
return self.last-self.first+1
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module HolmesLabs::Acts::FluxCapacitor::ActiveRecord::OperateOnDurations
|
2
|
+
def elapsed(time = Time.now)
|
3
|
+
time_since_beginning = self.class.length_of_time_since(self.beginning,time)
|
4
|
+
self.now?(time) ? time_since_beginning : nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def remaining(time = Time.now)
|
8
|
+
time_until_end = self.class.length_of_time_until(self.ending,time)
|
9
|
+
self.now?(time) ? time_until_end : nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def percent_complete(time = Time.now)
|
13
|
+
amount_complete = self.elapsed(time)
|
14
|
+
amount_complete ? 100 * ((amount_complete * 1.0) / duration) : nil
|
15
|
+
end
|
16
|
+
alias percent_elapsed percent_complete
|
17
|
+
alias percent_finished percent_complete
|
18
|
+
|
19
|
+
def percent_remaining(time = Time.now)
|
20
|
+
amount_remaining = self.remaining(time)
|
21
|
+
amount_remaining ? 100 * (( amount_remaining * 1.0) / duration) : nil
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module HolmesLabs::Acts::FluxCapacitor::ActiveRecord::OperateOnEvents
|
2
|
+
def at?(obj=Time.now)
|
3
|
+
bigger_than = self.start_time.before?(extract_time(obj,:start_time)) &&
|
4
|
+
self.end_time.after?( extract_time(obj,:end_time ))
|
5
|
+
smaller_than = self.start_time.after?( extract_time(obj,:start_time)) &&
|
6
|
+
self.end_time.before?( extract_time(obj,:end_time))
|
7
|
+
same_size = self.start_time == extract_time(obj,:start_time) &&
|
8
|
+
self.end_time == extract_time(obj,:end_time)
|
9
|
+
return bigger_than || smaller_than || same_size
|
10
|
+
end
|
11
|
+
alias happening_at? at?
|
12
|
+
alias happened_at? at?
|
13
|
+
|
14
|
+
def happening_now?(time = Time.now)
|
15
|
+
at?(time)
|
16
|
+
end
|
17
|
+
alias in_progress? happening_now?
|
18
|
+
alias now? happening_now?
|
19
|
+
alias present? happening_now?
|
20
|
+
|
21
|
+
def started?(time = Time.now)
|
22
|
+
self.start_time.before?(time)
|
23
|
+
end
|
24
|
+
alias began? started?
|
25
|
+
|
26
|
+
def ended?(time = Time.now)
|
27
|
+
self.end_time.before?(time)
|
28
|
+
end
|
29
|
+
alias finished? ended?
|
30
|
+
|
31
|
+
def <=>(obj)
|
32
|
+
if self.before?(obj)
|
33
|
+
return -1
|
34
|
+
elsif at?(obj)
|
35
|
+
return 0
|
36
|
+
elsif self.after?(obj)
|
37
|
+
return 1
|
38
|
+
else
|
39
|
+
return 'acts_as_flux_capacitor: non-fatal error in #<=>'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def transpose(duration)
|
44
|
+
new_event = self.clone
|
45
|
+
new_event.transpose!(duration)
|
46
|
+
end
|
47
|
+
alias shift transpose
|
48
|
+
alias reschedule transpose
|
49
|
+
alias move transpose
|
50
|
+
|
51
|
+
def transpose!(duration)
|
52
|
+
self.beginning += duration
|
53
|
+
self.ending += duration
|
54
|
+
return self
|
55
|
+
end
|
56
|
+
alias shift! transpose!
|
57
|
+
alias reschedule! transpose!
|
58
|
+
alias move! transpose!
|
59
|
+
end
|
60
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module HolmesLabs::Acts::FluxCapacitor::ActiveRecord::InstanceMethods::QueryGenerators #:nodoc:
|
2
|
+
|
3
|
+
private
|
4
|
+
|
5
|
+
def sql_before(some_time, current_time)
|
6
|
+
# table_name comes from ActiveRecord.
|
7
|
+
"#{table_name}.ends_at < '#{some_time.to_s(:db)}'"
|
8
|
+
end
|
9
|
+
alias sql_to sql_before
|
10
|
+
alias sql_ends_before sql_before
|
11
|
+
|
12
|
+
def sql_after(some_time , current_time)
|
13
|
+
# table_name comes from ActiveRecord.
|
14
|
+
"#{table_name}.begins_at > '#{some_time.to_s(:db)}'"
|
15
|
+
end
|
16
|
+
alias sql_from sql_after
|
17
|
+
alias sql_begins_after sql_after
|
18
|
+
|
19
|
+
def sql_at(some_time , current_time)
|
20
|
+
sql_join_conditions([ sql_starts_before( some_time , current_time),
|
21
|
+
sql_ends_after( some_time , current_time)])
|
22
|
+
end
|
23
|
+
|
24
|
+
def sql_ends_after(some_time, current_time)
|
25
|
+
"#{table_name}.ends_at > '#{some_time.to_s(:db)}'"
|
26
|
+
end
|
27
|
+
|
28
|
+
def sql_starts_before(some_time , current_time)
|
29
|
+
"#{table_name}.begins_at < '#{some_time.to_s(:db)}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
def sql_past(is_true , current_time)
|
33
|
+
if is_true
|
34
|
+
sql_before(current_time, current_time)
|
35
|
+
else
|
36
|
+
sql_join_conditions_or([ sql_present( current_time),
|
37
|
+
sql_future( current_time)])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias sql_ended sql_past
|
41
|
+
|
42
|
+
def sql_present(is_true , current_time)
|
43
|
+
if is_true
|
44
|
+
sql_at(current_time,current_time)
|
45
|
+
else
|
46
|
+
sql_join_conditions_or([ sql_past( current_time),
|
47
|
+
sql_future( current_time)])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias sql_now sql_present
|
51
|
+
|
52
|
+
def sql_future(is_true, current_time)
|
53
|
+
if is_true
|
54
|
+
sql_after(current_time,current_time)
|
55
|
+
else
|
56
|
+
sql_join_conditions_or([ sql_present(current_time),
|
57
|
+
sql_past( current_time)])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def sql_oldest_first(attrib = :begins_at)
|
62
|
+
"#{table_name}.#{attrib.to_s} ASC"
|
63
|
+
end
|
64
|
+
|
65
|
+
def sql_join_conditions(conditions)
|
66
|
+
"#{conditions.join(") AND (")}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def sql_join_conditions_or(conditions)
|
70
|
+
"#{conditions.join(") OR (")}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def sql_generate_temporal temporal_conditions , current_time
|
74
|
+
sql_conditions = []
|
75
|
+
temporal_conditions.each do |condition,parameter|
|
76
|
+
sql_conditions << send("sql_#{condition}".to_sym , parameter, current_time)
|
77
|
+
end
|
78
|
+
|
79
|
+
sql_join_conditions(sql_conditions)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module HolmesLabs::Acts::FluxCapacitor::ActiveRecord::Validation #:nodoc:
|
2
|
+
|
3
|
+
def validate
|
4
|
+
if self.begins_at.nil?
|
5
|
+
errors.add(:begins_at, "must have a start time")
|
6
|
+
if self.ends_at.nil?
|
7
|
+
errors.add(:ends_at, "must have an end time")
|
8
|
+
end
|
9
|
+
else
|
10
|
+
self.beginning.after?(self.ending) ? errors.add_to_base("An event cannot end before it begins") : nil
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,12 +1,11 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require 'rubygems'
|
4
|
-
require 'activerecord'
|
5
|
-
require 'date'
|
6
|
-
|
7
|
-
require File.join(SUBDIRECTORY, 'version')
|
8
|
-
require File.join(SUBDIRECTORY, 'temporal')
|
9
|
-
require File.join(SUBDIRECTORY, 'range_overlap')
|
10
|
-
require File.join(SUBDIRECTORY, 'core_class_extensions')
|
11
|
-
require File.join(SUBDIRECTORY, 'acts_as_flux_capacitor')
|
1
|
+
LIB_DIRECTORY = *['lib','acts_as_flux_capacitor']
|
12
2
|
|
3
|
+
require File.join(LIB_DIRECTORY, 'version')
|
4
|
+
require File.join(LIB_DIRECTORY, 'temporal')
|
5
|
+
require File.join(LIB_DIRECTORY, 'core_class_extensions')
|
6
|
+
require File.join(LIB_DIRECTORY, 'base')
|
7
|
+
require File.join(LIB_DIRECTORY, 'sql')
|
8
|
+
require File.join(LIB_DIRECTORY, 'validation')
|
9
|
+
require File.join(LIB_DIRECTORY, 'operate_on_events')
|
10
|
+
require File.join(LIB_DIRECTORY, 'operate_on_durations')
|
11
|
+
require File.join(LIB_DIRECTORY, 'compare_events')
|
data/spec/report.html
CHANGED
@@ -289,14 +289,19 @@ a {
|
|
289
289
|
</div>
|
290
290
|
<div class="example_group">
|
291
291
|
<dl>
|
292
|
-
<dt id="example_group_8">The extensions to
|
292
|
+
<dt id="example_group_8">The extensions to</dt>
|
293
|
+
</dl>
|
294
|
+
</div>
|
295
|
+
<div class="example_group">
|
296
|
+
<dl>
|
297
|
+
<dt id="example_group_9">The extensions to the Time class</dt>
|
293
298
|
<script type="text/javascript">moveProgressBar('97.5');</script>
|
294
299
|
<dd class="spec passed"><span class="passed_spec_name">should determine if two times are approximately equal (tolerance @ 1 second)</span></dd>
|
295
300
|
<script type="text/javascript">moveProgressBar('100.0');</script>
|
296
301
|
<dd class="spec passed"><span class="passed_spec_name">should determine if two times are approximately equal (tolerance @ 0.1 seconds)</span></dd>
|
297
302
|
</dl>
|
298
303
|
</div>
|
299
|
-
<script type="text/javascript">document.getElementById('duration').innerHTML = "Finished in <strong>0.
|
304
|
+
<script type="text/javascript">document.getElementById('duration').innerHTML = "Finished in <strong>0.424051 seconds</strong>";</script>
|
300
305
|
<script type="text/javascript">document.getElementById('totals').innerHTML = "40 examples, 0 failures";</script>
|
301
306
|
</div>
|
302
307
|
</div>
|
data/website/index.html
CHANGED
@@ -33,7 +33,7 @@
|
|
33
33
|
<h1>acts_as_flux_capacitor</h1>
|
34
34
|
<div id="version" class="clickable" onclick='document.location = "http://rubyforge.org/projects/acts_as_flux_capacitor"; return false'>
|
35
35
|
<p>Get Version</p>
|
36
|
-
<a href="http://rubyforge.org/projects/acts_as_flux_capacitor" class="numbers">0.
|
36
|
+
<a href="http://rubyforge.org/projects/acts_as_flux_capacitor" class="numbers">0.6.4</a>
|
37
37
|
</div>
|
38
38
|
<h1>→ ‘acts_as_flux_capacitor’</h1>
|
39
39
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_flux_capacitor
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jim Cropcho
|
@@ -9,10 +9,19 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-07-11 00:00:00 -04:00
|
13
13
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hoe
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.7.0
|
24
|
+
version:
|
16
25
|
description: Acts as Flux Capacitor is a better way to work with time-centric ActiveRecord models. Make it feel good to manipulate objects representing real-world events and/or objects with a finite period of database persistence!
|
17
26
|
email:
|
18
27
|
- jim.cropcho@gmail.com
|
@@ -37,9 +46,13 @@ files:
|
|
37
46
|
- config/hoe.rb
|
38
47
|
- config/requirements.rb
|
39
48
|
- lib/acts_as_flux_capacitor.rb
|
40
|
-
- lib/acts_as_flux_capacitor/
|
49
|
+
- lib/acts_as_flux_capacitor/base.rb
|
50
|
+
- lib/acts_as_flux_capacitor/sql.rb
|
51
|
+
- lib/acts_as_flux_capacitor/compare_events.rb
|
52
|
+
- lib/acts_as_flux_capacitor/operate_on_events.rb
|
53
|
+
- lib/acts_as_flux_capacitor/operate_on_durations.rb
|
41
54
|
- lib/acts_as_flux_capacitor/core_class_extensions.rb
|
42
|
-
- lib/acts_as_flux_capacitor/
|
55
|
+
- lib/acts_as_flux_capacitor/validation.rb
|
43
56
|
- lib/acts_as_flux_capacitor/temporal.rb
|
44
57
|
- lib/acts_as_flux_capacitor/version.rb
|
45
58
|
- script/console
|
@@ -88,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
101
|
requirements: []
|
89
102
|
|
90
103
|
rubyforge_project: aafc
|
91
|
-
rubygems_version: 1.
|
104
|
+
rubygems_version: 1.2.0
|
92
105
|
signing_key:
|
93
106
|
specification_version: 2
|
94
107
|
summary: Acts as Flux Capacitor is a better way to work with time-centric ActiveRecord models. Make it feel good to manipulate objects representing real-world events and/or objects with a finite period of database persistence!
|
@@ -1,341 +0,0 @@
|
|
1
|
-
module HolmesLabs #:nodoc:
|
2
|
-
module Acts #:nodoc:
|
3
|
-
module FluxCapacitor
|
4
|
-
module ActiveRecord #:nodoc:
|
5
|
-
def self.included(base) # :nodoc:
|
6
|
-
base.extend ClassMethods
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def acts_as_flux_capacitor
|
11
|
-
unless acts_as_flux_capacitor?
|
12
|
-
class << self
|
13
|
-
alias_method :original_find, :find
|
14
|
-
end
|
15
|
-
end
|
16
|
-
include Temporal
|
17
|
-
include InstanceMethods
|
18
|
-
include OperateOnEvents
|
19
|
-
include OperateOnDurations
|
20
|
-
include CompareEvents
|
21
|
-
include Validation
|
22
|
-
end
|
23
|
-
|
24
|
-
def flux_capacitor?
|
25
|
-
included_modules.include?(InstanceMethods)
|
26
|
-
end
|
27
|
-
alias acts_as_flux_capacitor? flux_capacitor?
|
28
|
-
end
|
29
|
-
|
30
|
-
protected
|
31
|
-
|
32
|
-
module InstanceMethods #:nodoc:
|
33
|
-
def self.included(base) # :nodoc:
|
34
|
-
base.extend ClassMethods
|
35
|
-
base.extend QueryGenerators
|
36
|
-
end
|
37
|
-
|
38
|
-
module ClassMethods
|
39
|
-
|
40
|
-
CUSTOM_FIND_OPTIONS = [
|
41
|
-
:before, # raw sql
|
42
|
-
:after, # raw sql
|
43
|
-
|
44
|
-
:now,
|
45
|
-
:at,
|
46
|
-
|
47
|
-
:past,
|
48
|
-
:present,
|
49
|
-
:future,
|
50
|
-
|
51
|
-
:from,
|
52
|
-
:to,
|
53
|
-
|
54
|
-
:starts_before, # raw sql
|
55
|
-
:ends_after # raw sql
|
56
|
-
]
|
57
|
-
|
58
|
-
def find(*args)
|
59
|
-
options = args.extract_options!
|
60
|
-
|
61
|
-
current_time = options.delete(:current_time) || Time.now
|
62
|
-
temporal_conditions = extract_temporal_conditions!(options)
|
63
|
-
sql_temporal = sql_generate_temporal(temporal_conditions,current_time)
|
64
|
-
|
65
|
-
with_scope( :find => {
|
66
|
-
:conditions => sql_temporal ,
|
67
|
-
:order => sql_oldest_first }) do
|
68
|
-
original_find(*(args << options))
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
def overlap(an_event,another_event)
|
73
|
-
an_event.to_range.overlap(another_event.to_range)
|
74
|
-
end
|
75
|
-
|
76
|
-
def overlap?(an_event,another_event)
|
77
|
-
an_event.to_range.overlap?(another_event.to_range)
|
78
|
-
end
|
79
|
-
|
80
|
-
private
|
81
|
-
|
82
|
-
def extract_temporal_conditions!(options)
|
83
|
-
temporal_conditions = {}
|
84
|
-
|
85
|
-
options.each do |key,val|
|
86
|
-
temporal_conditions.merge!({key, options.delete(key)}) if CUSTOM_FIND_OPTIONS.include?(key)
|
87
|
-
end
|
88
|
-
|
89
|
-
temporal_conditions
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
module QueryGenerators
|
94
|
-
private
|
95
|
-
|
96
|
-
def sql_before(some_time, current_time)
|
97
|
-
# table_name comes from ActiveRecord.
|
98
|
-
"#{table_name}.ends_at < '#{some_time.to_s(:db)}'"
|
99
|
-
end
|
100
|
-
alias sql_to sql_before
|
101
|
-
alias sql_ends_before sql_before
|
102
|
-
|
103
|
-
def sql_after(some_time , current_time)
|
104
|
-
# table_name comes from ActiveRecord.
|
105
|
-
"#{table_name}.begins_at > '#{some_time.to_s(:db)}'"
|
106
|
-
end
|
107
|
-
alias sql_from sql_after
|
108
|
-
alias sql_begins_after sql_after
|
109
|
-
|
110
|
-
def sql_at(some_time , current_time)
|
111
|
-
sql_join_conditions([ sql_starts_before( some_time , current_time),
|
112
|
-
sql_ends_after( some_time , current_time)])
|
113
|
-
end
|
114
|
-
|
115
|
-
def sql_ends_after(some_time, current_time)
|
116
|
-
"#{table_name}.ends_at > '#{some_time.to_s(:db)}'"
|
117
|
-
end
|
118
|
-
|
119
|
-
def sql_starts_before(some_time , current_time)
|
120
|
-
"#{table_name}.begins_at < '#{some_time.to_s(:db)}'"
|
121
|
-
end
|
122
|
-
|
123
|
-
def sql_past(is_true , current_time)
|
124
|
-
if is_true
|
125
|
-
sql_before(current_time, current_time)
|
126
|
-
else
|
127
|
-
sql_join_conditions_or([ sql_present( current_time),
|
128
|
-
sql_future( current_time)])
|
129
|
-
end
|
130
|
-
end
|
131
|
-
alias sql_ended sql_past
|
132
|
-
|
133
|
-
def sql_present(is_true , current_time)
|
134
|
-
if is_true
|
135
|
-
sql_at(current_time,current_time)
|
136
|
-
else
|
137
|
-
sql_join_conditions_or([ sql_past( current_time),
|
138
|
-
sql_future( current_time)])
|
139
|
-
end
|
140
|
-
end
|
141
|
-
alias sql_now sql_present
|
142
|
-
|
143
|
-
def sql_future(is_true, current_time)
|
144
|
-
if is_true
|
145
|
-
sql_after(current_time,current_time)
|
146
|
-
else
|
147
|
-
sql_join_conditions_or([ sql_present(current_time),
|
148
|
-
sql_past( current_time)])
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def sql_oldest_first(attrib = :begins_at)
|
153
|
-
"#{table_name}.#{attrib.to_s} ASC"
|
154
|
-
end
|
155
|
-
|
156
|
-
def sql_join_conditions(conditions)
|
157
|
-
"#{conditions.join(") AND (")}"
|
158
|
-
end
|
159
|
-
|
160
|
-
def sql_join_conditions_or(conditions)
|
161
|
-
"#{conditions.join(") OR (")}"
|
162
|
-
end
|
163
|
-
|
164
|
-
def sql_generate_temporal temporal_conditions , current_time
|
165
|
-
sql_conditions = []
|
166
|
-
temporal_conditions.each do |condition,parameter|
|
167
|
-
sql_conditions << send("sql_#{condition}".to_sym , parameter, current_time)
|
168
|
-
end
|
169
|
-
|
170
|
-
sql_join_conditions(sql_conditions)
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
def to_range
|
175
|
-
(start..finish)
|
176
|
-
end
|
177
|
-
alias range to_range
|
178
|
-
|
179
|
-
def start ; read_attribute(:begins_at) ; end
|
180
|
-
def start=(val) ; write_attribute(:begins_at,val); end
|
181
|
-
|
182
|
-
def finish ; read_attribute(:ends_at) ; end
|
183
|
-
def finish=(val) ; write_attribute(:ends_at,val) ; end
|
184
|
-
|
185
|
-
alias start_time start
|
186
|
-
alias start_time= start=
|
187
|
-
alias beginning start
|
188
|
-
alias beginning= start=
|
189
|
-
alias end_time finish
|
190
|
-
alias end_time= finish=
|
191
|
-
alias from start
|
192
|
-
alias from= start=
|
193
|
-
alias to finish
|
194
|
-
alias to= finish=
|
195
|
-
alias ending finish
|
196
|
-
alias ending= finish=
|
197
|
-
end # InstanceMethods
|
198
|
-
|
199
|
-
module OperateOnEvents
|
200
|
-
def at?(obj=Time.now)
|
201
|
-
bigger_than = self.start_time.before?(extract_time(obj,:start_time)) &&
|
202
|
-
self.end_time.after?( extract_time(obj,:end_time ))
|
203
|
-
smaller_than = self.start_time.after?( extract_time(obj,:start_time)) &&
|
204
|
-
self.end_time.before?( extract_time(obj,:end_time))
|
205
|
-
same_size = self.start_time == extract_time(obj,:start_time) &&
|
206
|
-
self.end_time == extract_time(obj,:end_time)
|
207
|
-
return bigger_than || smaller_than || same_size
|
208
|
-
end
|
209
|
-
alias happening_at? at?
|
210
|
-
alias happened_at? at?
|
211
|
-
|
212
|
-
def happening_now?(time = Time.now)
|
213
|
-
at?(time)
|
214
|
-
end
|
215
|
-
alias in_progress? happening_now?
|
216
|
-
alias now? happening_now?
|
217
|
-
alias present? happening_now?
|
218
|
-
|
219
|
-
def started?(time = Time.now)
|
220
|
-
self.start_time.before?(time)
|
221
|
-
end
|
222
|
-
alias began? started?
|
223
|
-
|
224
|
-
def ended?(time = Time.now)
|
225
|
-
self.end_time.before?(time)
|
226
|
-
end
|
227
|
-
alias finished? ended?
|
228
|
-
|
229
|
-
def <=>(obj)
|
230
|
-
if self.before?(obj)
|
231
|
-
return -1
|
232
|
-
elsif at?(obj)
|
233
|
-
return 0
|
234
|
-
elsif self.after?(obj)
|
235
|
-
return 1
|
236
|
-
else
|
237
|
-
return 'ActsAsEvent: non-fatal error in #status'
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
def transpose(duration)
|
242
|
-
new_event = self.clone
|
243
|
-
new_event.transpose!(duration)
|
244
|
-
end
|
245
|
-
alias shift transpose
|
246
|
-
alias reschedule transpose
|
247
|
-
alias move transpose
|
248
|
-
|
249
|
-
def transpose!(duration)
|
250
|
-
self.beginning += duration
|
251
|
-
self.ending += duration
|
252
|
-
return self
|
253
|
-
end
|
254
|
-
alias shift! transpose!
|
255
|
-
alias reschedule! transpose!
|
256
|
-
alias move! transpose!
|
257
|
-
end # OperateOnEvents
|
258
|
-
|
259
|
-
module OperateOnDurations
|
260
|
-
def elapsed(time = Time.now)
|
261
|
-
time_since_beginning = self.class.length_of_time_since(self.beginning,time)
|
262
|
-
self.now?(time) ? time_since_beginning : nil
|
263
|
-
end
|
264
|
-
|
265
|
-
def remaining(time = Time.now)
|
266
|
-
time_until_end = self.class.length_of_time_until(self.ending,time)
|
267
|
-
self.now?(time) ? time_until_end : nil
|
268
|
-
end
|
269
|
-
|
270
|
-
def percent_complete(time = Time.now)
|
271
|
-
amount_complete = self.elapsed(time)
|
272
|
-
amount_complete ? 100 * ((amount_complete * 1.0) / duration) : nil
|
273
|
-
end
|
274
|
-
alias percent_elapsed percent_complete
|
275
|
-
alias percent_finished percent_complete
|
276
|
-
|
277
|
-
def percent_remaining(time = Time.now)
|
278
|
-
amount_remaining = self.remaining(time)
|
279
|
-
amount_remaining ? 100 * (( amount_remaining * 1.0) / duration) : nil
|
280
|
-
end
|
281
|
-
end # OperateOnDurations
|
282
|
-
|
283
|
-
module CompareEvents
|
284
|
-
def overlaps_with?(other_event)
|
285
|
-
self.to_range.overlaps?(other_event.to_range)
|
286
|
-
end
|
287
|
-
|
288
|
-
def overlap_with(other_event)
|
289
|
-
(self.to_range.overlap(other_event.to_range)).size
|
290
|
-
end
|
291
|
-
alias overlap overlap_with
|
292
|
-
|
293
|
-
def back_to_back_with?(other_event,tolerance = 2.seconds)
|
294
|
-
# tolerance is in seconds.
|
295
|
-
self.start_time.before?(other_event.start_time) ? first_to_start = self : first_to_start = other_event
|
296
|
-
first_to_start.ends_at.approx_eql?(other_event.begins_at,tolerance)
|
297
|
-
end
|
298
|
-
|
299
|
-
def duration
|
300
|
-
self.class.length_of_time_until(self.ending,self.beginning)
|
301
|
-
end
|
302
|
-
alias length_of_time duration
|
303
|
-
alias length duration
|
304
|
-
|
305
|
-
def duration=(new_duration)
|
306
|
-
self.ending = self.beginning + new_duration
|
307
|
-
end
|
308
|
-
alias length_of_time= duration=
|
309
|
-
alias length= duration=
|
310
|
-
end # CompareEvents
|
311
|
-
|
312
|
-
module Validation
|
313
|
-
|
314
|
-
def validate
|
315
|
-
if self.begins_at.nil?
|
316
|
-
errors.add(:begins_at, "must have a start time")
|
317
|
-
if self.ends_at.nil?
|
318
|
-
errors.add(:ends_at, "must have an end time")
|
319
|
-
end
|
320
|
-
else
|
321
|
-
self.beginning.after?(self.ending) ? errors.add_to_base("An event cannot end before it begins") : nil
|
322
|
-
end
|
323
|
-
end
|
324
|
-
end
|
325
|
-
end # ActiveRecord
|
326
|
-
end # FluxCapacitor
|
327
|
-
end # Acts
|
328
|
-
end # HolmesLabs
|
329
|
-
ActiveRecord::Base.send :include, HolmesLabs::Acts::FluxCapacitor::ActiveRecord
|
330
|
-
|
331
|
-
# TODO
|
332
|
-
###########
|
333
|
-
# allow events to be created by start time and duration
|
334
|
-
# force exclusivity of events at any given time via parameter to acts_as_event()
|
335
|
-
# add new validations
|
336
|
-
# rake tasks
|
337
|
-
|
338
|
-
|
339
|
-
# NOTES
|
340
|
-
###########
|
341
|
-
# verify #find_* returns ordered results unless specified otherwise.
|
@@ -1,42 +0,0 @@
|
|
1
|
-
class Range
|
2
|
-
|
3
|
-
# the overlaps?(range) method comes from ActiveSupport::CoreExtensions::Range
|
4
|
-
# thanks to Nick Ang for suggesting its use.
|
5
|
-
# most of its tests were written by Jack Christensen of
|
6
|
-
# http://www.jackchristensen.com/article/2/detecting-overlapping-ranges
|
7
|
-
|
8
|
-
# Returns true if ranges overlap, false otherwise
|
9
|
-
def overlaps? other
|
10
|
-
include?(other.first) || other.include?(first)
|
11
|
-
end
|
12
|
-
|
13
|
-
# all of this added by jim cropcho:
|
14
|
-
|
15
|
-
def overlap(other_range)
|
16
|
-
return 0 unless self.overlaps?(other_range)
|
17
|
-
([self.first,other_range.first].max)..([self.last,other_range.last].min)
|
18
|
-
end
|
19
|
-
|
20
|
-
def size
|
21
|
-
if self.first.is_a?(Time) && self.last.is_a?(Time)
|
22
|
-
time_size
|
23
|
-
elsif self.first.is_a?(Fixnum) || self.first.is_a?(Float) &&
|
24
|
-
self.last.is_a?(Fixnum) || self.last.is_a?(Float)
|
25
|
-
number_size
|
26
|
-
else
|
27
|
-
raise "size cannot handle these object types."
|
28
|
-
end
|
29
|
-
end
|
30
|
-
alias length size
|
31
|
-
|
32
|
-
private
|
33
|
-
|
34
|
-
def time_size
|
35
|
-
raise "both endpoints of Range must be Time objects." unless self.last.is_a?(Time) && self.first.is_a?(Time)
|
36
|
-
return self.last-self.first
|
37
|
-
end
|
38
|
-
|
39
|
-
def number_size # technically, off by infinity^(-1) when either or both endpoints are excluded. meh.
|
40
|
-
return self.last-self.first+1
|
41
|
-
end
|
42
|
-
end
|