chronological 0.0.9 → 1.0.0beta1
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/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
|
+
[](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
|
+

|
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
|
+

|
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
|
+
|