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