chronological 0.0.9 → 1.0.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +321 -17
- data/lib/chronological/errors.rb +3 -0
- data/lib/chronological/strategies/absolute.rb +44 -0
- data/lib/chronological/strategies/base.rb +82 -0
- data/lib/chronological/strategies/relative.rb +68 -0
- data/lib/chronological/strategies.rb +3 -0
- data/lib/chronological/strategy_resolver.rb +97 -0
- data/lib/chronological/version.rb +1 -1
- data/lib/chronological.rb +93 -3
- data/spec/chronological_spec.rb +33 -0
- data/spec/spec_helper.rb +1 -47
- data/spec/{absolute_timeframe_spec.rb → strategies/absolute_spec.rb} +120 -83
- data/spec/strategies/base_spec.rb +296 -0
- data/spec/{relative_timeframe_spec.rb → strategies/relative_spec.rb} +212 -16
- data/spec/strategy_resolver_spec.rb +56 -0
- data/spec/support/active_record.rb +47 -0
- metadata +26 -17
- data/lib/chronological/absolute_timeframe.rb +0 -98
- data/lib/chronological/base.rb +0 -41
- data/lib/chronological/relative_timeframe.rb +0 -96
- data/spec/base_spec.rb +0 -230
data/README.md
CHANGED
@@ -1,29 +1,333 @@
|
|
1
|
-
|
1
|
+
Chronological: The 'Start' of Your Fun Will Never 'End'
|
2
|
+
================================================================================
|
2
3
|
|
3
|
-
|
4
|
+
[![Build Status](https://secure.travis-ci.org/chirrpy/chronological.png?branch=master)](http://travis-ci.org/chirrpy/chronological)
|
4
5
|
|
5
|
-
|
6
|
+
Chronological is your one-stop solution for handling time ranges in your classes.
|
6
7
|
|
7
|
-
|
8
|
+
Have an item that is only available between two dates/times? In these
|
9
|
+
situations, there is quite a bit of logic which is common among use
|
10
|
+
cases and that's what Chronological provides.
|
8
11
|
|
9
|
-
|
12
|
+
![Clock Tower Flier](http://www.thekompanee.com/public_files/clock-tower-flier-small.png)
|
10
13
|
|
11
|
-
|
14
|
+
Supported Rubies
|
15
|
+
--------------------------------------------------------------------------------
|
16
|
+
* MRI Ruby 1.9.2
|
17
|
+
* MRI Ruby 1.9.3
|
18
|
+
* JRuby (in 1.9 compat mode)
|
12
19
|
|
13
|
-
|
20
|
+
Installation
|
21
|
+
--------------------------------------------------------------------------------
|
14
22
|
|
15
|
-
|
23
|
+
First:
|
16
24
|
|
17
|
-
|
25
|
+
```ruby
|
26
|
+
gem install chronological
|
27
|
+
```
|
18
28
|
|
19
|
-
|
29
|
+
Then in your script:
|
20
30
|
|
21
|
-
|
31
|
+
```ruby
|
32
|
+
require 'chronological'
|
33
|
+
```
|
22
34
|
|
23
|
-
|
35
|
+
Finally in your class:
|
24
36
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
37
|
+
```ruby
|
38
|
+
include Chronological
|
39
|
+
```
|
40
|
+
|
41
|
+
or from IRB
|
42
|
+
|
43
|
+
irb -r 'chronological'
|
44
|
+
|
45
|
+
Basic Usage
|
46
|
+
--------------------------------------------------------------------------------
|
47
|
+
|
48
|
+
The easiest way to use Chronological is to just let it know which [strategy](#strategies)
|
49
|
+
you would like to use for the time range for that particular object.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
class MyTimeRangeClass
|
53
|
+
include Chronological
|
54
|
+
|
55
|
+
timeframe :type => :absolute
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
This will look for a `started_at` and an `ended_at` method on your object and
|
60
|
+
will base all of its dynamic methods on those.
|
61
|
+
|
62
|
+
Now you can use any of the methods that Chronological adds to your class:
|
63
|
+
|
64
|
+
--------------------------------------------------------------------------------
|
65
|
+
### Range Status Predicates
|
66
|
+
|
67
|
+
* `started?` - Whether it is currently on or after the start date
|
68
|
+
* `ended?` - Whether it is currently on or after the end date
|
69
|
+
* `not_yet_ended?` - Whether it is currently before the end date
|
70
|
+
* `in_progress?` _(aliased to `active?`)_ - Whether it is between the start and end dates
|
71
|
+
* `inactive?` - The inverse of `in_progress?`/`active?`
|
72
|
+
|
73
|
+
--------------------------------------------------------------------------------
|
74
|
+
### Scheduling Validity Predicates
|
75
|
+
|
76
|
+
* `scheduled?` - Whether all of the pieces needed to determine a timeframe have been set
|
77
|
+
* `partially_scheduled?` - Whether _any_ of the pieces needed to determine a timeframe have been set
|
78
|
+
|
79
|
+
--------------------------------------------------------------------------------
|
80
|
+
### Range Type Predicates
|
81
|
+
|
82
|
+
* `same_day?` - Whether the starting time and the ending time are on the same day
|
83
|
+
* `one_day?` - Whether the starting time and the ending time are less than 24 hours apart
|
84
|
+
* `multi_day?` - Whether the starting time and the ending time are on different days
|
85
|
+
|
86
|
+
--------------------------------------------------------------------------------
|
87
|
+
### Other Methods
|
88
|
+
|
89
|
+
* `duration` - A Hash containing the days, hours, minutes and seconds
|
90
|
+
|
91
|
+
--------------------------------------------------------------------------------
|
92
|
+
### Scopes
|
93
|
+
|
94
|
+
Each of these methods also has a corresponding ActiveRelation method that will
|
95
|
+
represents all of the items that meet the requirements of that method.
|
96
|
+
|
97
|
+
* `started`
|
98
|
+
* `ended`
|
99
|
+
* `not_yet_ended`
|
100
|
+
* `in_progress` _(also aliased to `active?`)_
|
101
|
+
* `scheduled`
|
102
|
+
* `partially_scheduled`
|
103
|
+
* `one_day`
|
104
|
+
* `multi_day`
|
105
|
+
|
106
|
+
#### Example
|
107
|
+
|
108
|
+
If this is true:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
range_instance = InstanceWithTimeRange.create
|
112
|
+
|
113
|
+
range_instance.started? #=> true
|
114
|
+
```
|
115
|
+
|
116
|
+
Then this would also be the case:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
expect { InstanceWithTimeRange.started }.to include range_instance #=> true
|
120
|
+
```
|
121
|
+
|
122
|
+
### Ordering
|
123
|
+
|
124
|
+
* `by_duration(:asc)` - Sort the results by the length of the duration
|
125
|
+
* `by_date(:asc)` - Intelligently sort based on the start and end date
|
126
|
+
|
127
|
+
Advanced Usage
|
128
|
+
--------------------------------------------------------------------------------
|
129
|
+
_Note 1: All durations and offsets are represented in seconds._
|
130
|
+
_Note 2: All 'Defaults' are in the same order as the 'Options' above them._
|
131
|
+
|
132
|
+
### Strategies
|
133
|
+
|
134
|
+
Chronological is extremely flexible and can handle multiple strategies for
|
135
|
+
calculating your time range information.
|
136
|
+
|
137
|
+
If you choose to use our default field names, you can simply pass the strategy
|
138
|
+
in as a symbol and it will Just Work(tm).
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
class MyTimeRangeClass
|
142
|
+
timeframe :type => :duration_until_end
|
143
|
+
end
|
144
|
+
```
|
145
|
+
|
146
|
+
Alternatively, you can pass in the specific set of options per strategy as a
|
147
|
+
hash and set the values to the field names you want Chronological to use.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
class MyTimeRangeClass
|
151
|
+
timeframe :ending_time => :available_until,
|
152
|
+
:duration => :length_of_availability
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
--------------------------------------------------------------------------------
|
157
|
+
#### Absolute
|
158
|
+
|
159
|
+
**Options:** `starting_time` and `ending_time`
|
160
|
+
|
161
|
+
**Defaults:** `started_at` and `ended_at`
|
162
|
+
|
163
|
+
**Example:** The concert starts at 5:30pm and ends at 11:30pm
|
164
|
+
|
165
|
+
--------------------------------------------------------------------------------
|
166
|
+
#### Relative
|
167
|
+
|
168
|
+
**Options:** `base_of_offset`, `starting_offset` and `ending_offset`
|
169
|
+
|
170
|
+
**Defaults:** `base_of_range_offset`, `start_of_range_offset` and `end_of_range_offset`
|
171
|
+
|
172
|
+
**Example:** You can buy the ticket anytime between 1-2 weeks before the event starts
|
173
|
+
|
174
|
+
--------------------------------------------------------------------------------
|
175
|
+
#### Dual Relative
|
176
|
+
|
177
|
+
**Options:** `base_of_starting_offset`, `starting_offset`, `base_of_ending_offset` and `ending_offset`
|
178
|
+
|
179
|
+
**Defaults:** `base_of_range_starting_offset`, `start_of_range_offset`, `base_of_range_ending_offset` and `end_of_range_offset`
|
180
|
+
|
181
|
+
**Example:** The coupon is active from 2 hours after the first sale until 3 hours before the store closes
|
182
|
+
|
183
|
+
--------------------------------------------------------------------------------
|
184
|
+
#### Duration From Start
|
185
|
+
|
186
|
+
**Options:** `starting_time`, `duration`
|
187
|
+
|
188
|
+
**Defaults:** `started_at` and `duration_in_seconds`
|
189
|
+
|
190
|
+
**Example:** The donut shop opens at 7am but only for 30 minutes
|
191
|
+
|
192
|
+
--------------------------------------------------------------------------------
|
193
|
+
#### Duration Until End
|
194
|
+
|
195
|
+
**Options:** `ending_time`, `duration`
|
196
|
+
|
197
|
+
**Defaults:** `ended_at` and `duration_in_seconds`
|
198
|
+
|
199
|
+
**Example:** Your 'roadie' pass will only get you backstage for the 30 minutes before the band goes on stage at 9pm
|
200
|
+
|
201
|
+
--------------------------------------------------------------------------------
|
202
|
+
#### Duration From Relative Start
|
203
|
+
|
204
|
+
**Options:** `base_of_starting_offset`, `starting_offset` and `duration`
|
205
|
+
|
206
|
+
**Defaults:** `base_of_range_starting_offset`, `start_of_range_offset` and `duration_in_seconds`
|
207
|
+
|
208
|
+
**Example:** The party will start 30 minutes after the concert ends and last for 4 hours
|
209
|
+
|
210
|
+
--------------------------------------------------------------------------------
|
211
|
+
#### Duration Until A Relative End
|
212
|
+
|
213
|
+
**Options:** `base_of_ending_offset`, `ending_offset` and `duration`
|
214
|
+
|
215
|
+
**Defaults:** `base_of_range_ending_offset`, `end_of_range_offset` and `duration_in_seconds`
|
216
|
+
|
217
|
+
**Example:** The secret vault will be open for 3 hours and will relock 15 minutes prior to the office opening
|
218
|
+
|
219
|
+
### Determining Absolute Dates
|
220
|
+
|
221
|
+
Other than the 'Absolute' strategy above, any of those combinations will
|
222
|
+
calculate the absolute dates of the time range and make them available via
|
223
|
+
these accessors:
|
224
|
+
|
225
|
+
* `started_at`
|
226
|
+
* `ended_at`
|
227
|
+
* `started_on`
|
228
|
+
* `ended_on`
|
229
|
+
|
230
|
+
If you want to override these values, simply pass in the
|
231
|
+
`absolute_start_date_field`, `absolute_end_date_field`,
|
232
|
+
`absolute_start_time_field` and/or `absolute_end_time_field` options to
|
233
|
+
`timeframe` like so:
|
234
|
+
|
235
|
+
#### Example
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
class MyTimeRangeClass
|
239
|
+
timeframe :base_of_offset => :event_start_time,
|
240
|
+
:starting_offset => :starting_availability_offset
|
241
|
+
:ending_offset => :ending_availability_offset,
|
242
|
+
:absolute_start_time_field => :starting_time
|
243
|
+
:absolute_end_time_field => :ending_time
|
244
|
+
end
|
245
|
+
```
|
246
|
+
|
247
|
+
### Advanced Method Usage
|
248
|
+
|
249
|
+
--------------------------------------------------------------------------------
|
250
|
+
#### Range Status As Of A Given Date
|
251
|
+
|
252
|
+
All range status methods can take an `:as_of` option which will replace the
|
253
|
+
default behavior which is `Time.now.utc`. Using this option you can more easily
|
254
|
+
see if an instance (or instances) would be started, ended, etc as of a given
|
255
|
+
date.
|
256
|
+
|
257
|
+
Affected methods:
|
258
|
+
|
259
|
+
* `started?`
|
260
|
+
* `ended?`
|
261
|
+
* `not_yet_ended?`
|
262
|
+
* `in_progress?` _(or `active?`)_
|
263
|
+
* `inactive?`
|
264
|
+
|
265
|
+
Affected scopes:
|
266
|
+
|
267
|
+
* `started`
|
268
|
+
* `ended`
|
269
|
+
* `not_yet_ended`
|
270
|
+
* `in_progress` _(or `active`)_
|
271
|
+
* `inactive?`
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
range_instance = MyTimeRangeClass.create
|
275
|
+
|
276
|
+
range_instance.ended? #=> false
|
277
|
+
range_instance.ended? :as_of => 42.years.from_now #=> true
|
278
|
+
|
279
|
+
MyTimeRangeClass.ended #=> []
|
280
|
+
MyTimeRangeClass.ended :as_of => 42.years.from_now #=> [range_instance]
|
281
|
+
```
|
282
|
+
|
283
|
+
## Is It Scheduled?
|
284
|
+
|
285
|
+
Even though Chronological does not handle anything having to do with time
|
286
|
+
zones, it is valid however, to assume there will be use cases where,
|
287
|
+
without a time zone, the model should not be considered `scheduled`.
|
288
|
+
|
289
|
+
If you wish to account for this, pass in the `:time_zone` option to
|
290
|
+
`timeframe` and give it the field name that contains the time zone you
|
291
|
+
wish to use. For example:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
class MyTimeRangeClass
|
295
|
+
timeframe :starting_time => :started_at,
|
296
|
+
:ending_time => :ended_at,
|
297
|
+
:time_zone => :time_zone
|
298
|
+
end
|
299
|
+
|
300
|
+
range_instance = MyTimeRangeClass.new
|
301
|
+
range_instance.started_at = 5.minutes.from_now
|
302
|
+
range_instance.ended_at = 30.minutes.from_now
|
303
|
+
|
304
|
+
range_instance.scheduled? # => false
|
305
|
+
|
306
|
+
range_instance.time_zone = 'Alaska'
|
307
|
+
|
308
|
+
range_instance.scheduled? # => true
|
309
|
+
```
|
310
|
+
|
311
|
+
Issues
|
312
|
+
--------------------------------
|
313
|
+
|
314
|
+
If you have problems, please create a [Github issue](https://github.com/chirrpy/chronological/issues).
|
315
|
+
|
316
|
+
Credits
|
317
|
+
--------------------------------
|
318
|
+
|
319
|
+
![chirrpy](https://dl.dropbox.com/s/f9s2qd0kmbc8nwl/github_logo.png?dl=1)
|
320
|
+
|
321
|
+
greenwich is maintained by [Chrrpy, LLC](http://chirrpy.com)
|
322
|
+
|
323
|
+
The names and logos for Chirrpy are trademarks of Chrrpy, LLC
|
324
|
+
|
325
|
+
Contributors
|
326
|
+
--------------------------------
|
327
|
+
* [Jeff Felchner](https://github.com/jfelchner)
|
328
|
+
* [Mark McEahern](https://github.com/m5rk)
|
329
|
+
|
330
|
+
License
|
331
|
+
--------------------------------
|
332
|
+
|
333
|
+
chronological is Copyright © 2012 Chirrpy. It is free software, and may be redistributed under the terms specified in the LICENSE file.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Chronological
|
2
|
+
class AbsoluteStrategy < BaseStrategy
|
3
|
+
def scheduled?(object)
|
4
|
+
object.send(field_names[:starting_time]).present? &&
|
5
|
+
object.send(field_names[:ending_time]).present? &&
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
9
|
+
def partially_scheduled?(object)
|
10
|
+
object.send(field_names[:starting_time]).present? ||
|
11
|
+
object.send(field_names[:ending_time]).present? ||
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_absolute_start?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def has_absolute_end?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def self.started_at_sql_calculation(field_names)
|
25
|
+
field_names[:starting_time]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.ended_at_sql_calculation(field_names)
|
29
|
+
field_names[:ending_time]
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.duration_sql_calculation(field_names)
|
33
|
+
"extract ('epoch' from (#{field_names[:ending_time]} - #{field_names[:starting_time]}))"
|
34
|
+
end
|
35
|
+
|
36
|
+
def duration_in_seconds(object)
|
37
|
+
(object.send(field_names[:ending_time]) - object.send(field_names[:starting_time]))
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_absolute_timeframe?(object)
|
41
|
+
object.send(field_names[:starting_time]).present? && object.send(field_names[:ending_time]).present?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Chronological
|
2
|
+
class BaseStrategy
|
3
|
+
attr_reader :field_names
|
4
|
+
|
5
|
+
def initialize(field_names = {})
|
6
|
+
@field_names = field_names.freeze
|
7
|
+
end
|
8
|
+
|
9
|
+
def starting_date(object)
|
10
|
+
return nil unless object.send(field_names[:starting_time]).respond_to? :to_date
|
11
|
+
|
12
|
+
object.send(field_names[:starting_time]).to_date
|
13
|
+
end
|
14
|
+
|
15
|
+
def ending_date(object)
|
16
|
+
return nil unless object.send(field_names[:ending_time]).respond_to? :to_date
|
17
|
+
|
18
|
+
object.send(field_names[:ending_time]).to_date
|
19
|
+
end
|
20
|
+
|
21
|
+
def inactive?(object)
|
22
|
+
!object.in_progress?
|
23
|
+
end
|
24
|
+
|
25
|
+
def duration(object)
|
26
|
+
calculated_duration = duration_in_seconds(object)
|
27
|
+
|
28
|
+
return Hash.new unless calculated_duration.present?
|
29
|
+
|
30
|
+
hours = (calculated_duration / 3600).to_i
|
31
|
+
minutes = ((calculated_duration % 3600) / 60).to_i
|
32
|
+
seconds = (calculated_duration % 60).to_i
|
33
|
+
|
34
|
+
{ :hours => hours, :minutes => minutes, :seconds => seconds }
|
35
|
+
end
|
36
|
+
|
37
|
+
def scheduled?(object)
|
38
|
+
!field_names[:time_zone].nil? ? object.send(field_names[:time_zone]) : true
|
39
|
+
end
|
40
|
+
|
41
|
+
def partially_scheduled?(object)
|
42
|
+
!field_names[:time_zone].nil? ? object.send(field_names[:time_zone]) : false
|
43
|
+
end
|
44
|
+
|
45
|
+
def in_progress?(object)
|
46
|
+
return false unless has_absolute_timeframe?(object)
|
47
|
+
|
48
|
+
(object.send(field_names[:starting_time]) <= Time.now.utc) && object.send(field_names[:ending_time]).future?
|
49
|
+
end
|
50
|
+
|
51
|
+
###
|
52
|
+
# Scopes
|
53
|
+
#
|
54
|
+
def self.started(object, field_names)
|
55
|
+
object.where "#{started_at_sql_calculation(field_names)} <= :as_of", :as_of => Time.now.utc
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.ended(object, field_names)
|
59
|
+
object.where "#{ended_at_sql_calculation(field_names)} <= :as_of", :as_of => Time.now.utc
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.not_yet_ended(object, field_names)
|
63
|
+
object.where "#{ended_at_sql_calculation(field_names)} > :as_of", :as_of => Time.now.utc
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.in_progress(object, field_names)
|
67
|
+
object.started.not_yet_ended
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.in_progress?(object, field_names)
|
71
|
+
object.in_progress.any?
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.by_date(object, field_names, direction)
|
75
|
+
object.order "#{object.table_name}.#{started_at_sql_calculation(field_names)} #{direction}, #{object.table_name}.#{ended_at_sql_calculation(field_names)} #{direction}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.by_duration(object, field_names, direction)
|
79
|
+
object.order "#{duration_sql_calculation(field_names)} #{direction}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Chronological
|
2
|
+
class RelativeStrategy < BaseStrategy
|
3
|
+
def starting_time(object)
|
4
|
+
return nil unless object.send(field_names[:base_of_offset]).present? && object.send(field_names[:starting_offset]).present?
|
5
|
+
|
6
|
+
object.send(field_names[:base_of_offset]) - object.send(field_names[:starting_offset])
|
7
|
+
end
|
8
|
+
|
9
|
+
def ending_time(object)
|
10
|
+
return nil unless object.send(field_names[:base_of_offset]).present? && object.send(field_names[:ending_offset]).present?
|
11
|
+
|
12
|
+
object.send(field_names[:base_of_offset]) - object.send(field_names[:ending_offset])
|
13
|
+
end
|
14
|
+
|
15
|
+
def scheduled?(object)
|
16
|
+
object.send(field_names[:base_of_offset]).present? &&
|
17
|
+
object.send(field_names[:starting_offset]).present? &&
|
18
|
+
object.send(field_names[:ending_offset]).present? &&
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def partially_scheduled?(object)
|
23
|
+
object.send(field_names[:base_of_offset]).present? ||
|
24
|
+
object.send(field_names[:starting_offset]).present? ||
|
25
|
+
object.send(field_names[:ending_offset]).present? ||
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.in_progress(object, field_names)
|
30
|
+
object.all.select(&:in_progress?)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.in_progress?(object, field_names)
|
34
|
+
object.all.any?(&:in_progress?)
|
35
|
+
end
|
36
|
+
|
37
|
+
def has_absolute_start?
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_absolute_end?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def self.started_at_sql_calculation(field_names)
|
47
|
+
"#{field_names[:base_of_offset]} - (#{field_names[:starting_offset]} * INTERVAL '1 seconds')"
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.ended_at_sql_calculation(field_names)
|
51
|
+
"#{field_names[:base_of_offset]} - (#{field_names[:ending_offset]} * INTERVAL '1 seconds')"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.duration_sql_calculation(field_names)
|
55
|
+
"#{field_names[:starting_offset]} - #{field_names[:ending_offset]}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def duration_in_seconds(object)
|
59
|
+
return nil unless object.send(field_names[:starting_offset]).present? && object.send(field_names[:ending_offset]).present?
|
60
|
+
|
61
|
+
object.send(field_names[:starting_offset]) - object.send(field_names[:ending_offset])
|
62
|
+
end
|
63
|
+
|
64
|
+
def has_absolute_timeframe?(object)
|
65
|
+
object.scheduled?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module Chronological
|
2
|
+
class StrategyResolver
|
3
|
+
STRATEGIES = {
|
4
|
+
absolute: Set[ :starting_time, :ending_time ],
|
5
|
+
relative: Set[ :base_of_offset, :starting_offset, :ending_offset ],
|
6
|
+
dual_relative: Set[ :base_of_starting_offset, :starting_offset, :base_of_ending_offset, :ending_offset ],
|
7
|
+
duration_from_start: Set[ :starting_time, :duration ],
|
8
|
+
duration_until_end: Set[ :ending_time, :duration ],
|
9
|
+
duration_from_relative_start: Set[ :base_of_starting_offset, :starting_offset, :duration ],
|
10
|
+
duration_until_a_relative_end: Set[ :base_of_ending_offset, :ending_offset, :duration ]
|
11
|
+
}
|
12
|
+
|
13
|
+
VALID_OPTIONS = [
|
14
|
+
:starting_time,
|
15
|
+
:ending_time,
|
16
|
+
:starting_date,
|
17
|
+
:ending_date,
|
18
|
+
:base_of_offset,
|
19
|
+
:starting_offset,
|
20
|
+
:ending_offset,
|
21
|
+
:base_of_starting_offset,
|
22
|
+
:base_of_ending_offset,
|
23
|
+
:duration,
|
24
|
+
:absolute_start_date_field,
|
25
|
+
:absolute_end_date_field,
|
26
|
+
:absolute_start_time_field,
|
27
|
+
:absolute_end_time_field,
|
28
|
+
]
|
29
|
+
|
30
|
+
CROSS_STRATEGY_OPTIONS = Set[
|
31
|
+
:starting_time,
|
32
|
+
:ending_time,
|
33
|
+
:starting_date,
|
34
|
+
:ending_date,
|
35
|
+
:time_zone
|
36
|
+
]
|
37
|
+
|
38
|
+
DEFAULT_FIELD_NAMES = [
|
39
|
+
:started_at,
|
40
|
+
:ended_at,
|
41
|
+
:started_on,
|
42
|
+
:ended_on,
|
43
|
+
:base_of_range_offset,
|
44
|
+
:start_of_range_offset,
|
45
|
+
:end_of_range_offset,
|
46
|
+
:base_of_range_starting_offset,
|
47
|
+
:base_of_range_ending_offset,
|
48
|
+
:duration_in_seconds,
|
49
|
+
:started_on,
|
50
|
+
:ended_on,
|
51
|
+
:started_at,
|
52
|
+
:ended_at
|
53
|
+
]
|
54
|
+
|
55
|
+
DEFAULT_FIELD_NAMES_FOR_OPTIONS = Hash[ VALID_OPTIONS.zip DEFAULT_FIELD_NAMES ]
|
56
|
+
|
57
|
+
def self.resolve(options)
|
58
|
+
strategy_name = resolve_strategy_name(options)
|
59
|
+
field_names = resolve_strategy_fields(strategy_name, options)
|
60
|
+
|
61
|
+
"Chronological::#{strategy_name.to_s.classify}Strategy".constantize.new(field_names)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
def self.resolve_strategy_fields(strategy_name, options)
|
66
|
+
strategy_option_names = STRATEGIES[strategy_name] + CROSS_STRATEGY_OPTIONS
|
67
|
+
overridden_options = DEFAULT_FIELD_NAMES_FOR_OPTIONS.merge options
|
68
|
+
|
69
|
+
overridden_options.select do |option_name, option_value|
|
70
|
+
strategy_option_names.include? option_name
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.resolve_strategy_name(options)
|
75
|
+
strategy_name = if options[:type]
|
76
|
+
options[:type]
|
77
|
+
else
|
78
|
+
resolve_strategy_name_from_options(options)
|
79
|
+
end
|
80
|
+
|
81
|
+
raise Chronological::UndefinedStrategy unless STRATEGIES.include? strategy_name
|
82
|
+
|
83
|
+
strategy_name
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.resolve_strategy_name_from_options(options)
|
87
|
+
option_names = Set.new options.keys
|
88
|
+
|
89
|
+
resolved_strategy = STRATEGIES.find do |strategy_name, required_options|
|
90
|
+
required_options & option_names == required_options
|
91
|
+
end
|
92
|
+
|
93
|
+
resolved_strategy ? resolved_strategy[0] : nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|