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 CHANGED
@@ -1,29 +1,333 @@
1
- # Chronological
1
+ Chronological: The 'Start' of Your Fun Will Never 'End'
2
+ ================================================================================
2
3
 
3
- TODO: Write a gem description
4
+ [![Build Status](https://secure.travis-ci.org/chirrpy/chronological.png?branch=master)](http://travis-ci.org/chirrpy/chronological)
4
5
 
5
- ## Installation
6
+ Chronological is your one-stop solution for handling time ranges in your classes.
6
7
 
7
- Add this line to your application's Gemfile:
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
- gem 'chronological'
12
+ ![Clock Tower Flier](http://www.thekompanee.com/public_files/clock-tower-flier-small.png)
10
13
 
11
- And then execute:
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
- $ bundle
20
+ Installation
21
+ --------------------------------------------------------------------------------
14
22
 
15
- Or install it yourself as:
23
+ First:
16
24
 
17
- $ gem install chronological
25
+ ```ruby
26
+ gem install chronological
27
+ ```
18
28
 
19
- ## Usage
29
+ Then in your script:
20
30
 
21
- TODO: Write usage instructions here
31
+ ```ruby
32
+ require 'chronological'
33
+ ```
22
34
 
23
- ## Contributing
35
+ Finally in your class:
24
36
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
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,3 @@
1
+ module Chronological
2
+ class UndefinedStrategy < RuntimeError; end
3
+ end
@@ -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,3 @@
1
+ require 'chronological/strategies/base'
2
+ require 'chronological/strategies/absolute'
3
+ require 'chronological/strategies/relative'
@@ -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
+
@@ -1,3 +1,3 @@
1
1
  module Chronological
2
- VERSION = '0.0.9'
2
+ VERSION = '1.0.0beta1'
3
3
  end