trackoid_mongoid4 0.1.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 394a49795942287b9f6a9fb49747c21c4a6c0b84
4
+ data.tar.gz: 6e8126f53f026badca56b404775b8bbdedb9fb9a
5
+ SHA512:
6
+ metadata.gz: bbe2bd55a986274f89ec38fec284252317178c734e5170fffd4d5f603ad4adcd401484d5693b610d261c75ac28d87615be0e83fc1a9fc07ffc21aee7b97215ee
7
+ data.tar.gz: 7398f8e2df15685d3992643c006bd89e6bbe716ca2240191eb53652443935a1e2decbca3702396a9af34afb8146b0aea1b0099828a9aa5eb995a5aa5eb551c74
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ Gemfile.lock
21
+
22
+ ## PROJECT::SPECIFIC
23
+ trackoid_mongoid4-0.1.0.gem
24
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ services: mongodb
3
+ rvm:
4
+ - 1.9.3
5
+ - 2.0.0
6
+ - rbx-19mode
7
+ - jruby-19mode
8
+ - jruby-head
9
+ - ruby-head
10
+ matrix:
11
+ allow_failures:
12
+ - rvm: jruby-head
13
+ - rvm: ruby-head
14
+ - rvm: 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Declare your gem's dependencies in trackoid.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
7
+
8
+ gem "mongoid", "~> 4.0.1"
9
+ gem 'byebug'
10
+ gem 'pry-byebug'
11
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015 David Bernard Krett
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,318 @@
1
+ # Trackoid
2
+
3
+ > Trackoid is an analytics tracking system made specifically for MongoDB using Mongoid as ORM this gem is forked from the [Trackoid Gem](https://github/twixter/trackoid) and has been updated to work with mongoid 4. Many thanks to @twoixter who created the original Trackoid gem for Mongoid 3.
4
+
5
+ [![Build Status](https://travis-ci.org/dbkbali/trackoid_mongoid4.png)](https://travis-ci.org/dbkbali/trackoid_mongoid4)
6
+
7
+ # Installation
8
+
9
+ Add gem 'trackoid_mongoid4'
10
+
11
+ to your gem file and bundle
12
+
13
+ # Requirements
14
+
15
+ Trackoid requires Mongoid version 4 or above, which obviously in turn requires MongoDB.
16
+
17
+ Please feel free to fork and port to other libraries. However, Trackoid really requires MongoDB since it is build from scratch to take advantage of several MongoDB features (please let me know if you dare enough to port Trackoid into CouchDB or similar, I will be glad to know).
18
+
19
+ # Using Trackoid to track analytics information for models
20
+
21
+ Given the most obvious use for Trackoid, consider this example:
22
+
23
+ Class WebPage
24
+ include Mongoid::Document
25
+ include Mongoid::Tracking
26
+
27
+ ...
28
+
29
+ track :visits
30
+ end
31
+
32
+ This class models a web page, and by using `track :visits` we add a `visits` field to track... well... visits. :-) Later, in out controller we can do:
33
+
34
+ def view
35
+ @page = WebPage.find(params[:webpage_id])
36
+
37
+ @page.visits.inc # Increment a visit to this page
38
+ end
39
+
40
+ That is, dead simple. Later in our views we can use the `visits` field to show the visit information to the users:
41
+
42
+ <h1><%= @page.visits.today %> visits to this page today</h1>
43
+ <p>The page had <%= @page.visits.yesterday %> visits yesterday</p>
44
+
45
+ Of course, you can also show visits in a time range:
46
+
47
+ <h1>Visits on last 7 days</h1>
48
+ <ul>
49
+ <% @page.visits.last_days(7).reverse.each_with_index do |d, i| %>
50
+ <li><%= (DateTime.now - i).to_s %> : <%= d %></li>
51
+ <% end %>
52
+ </ul>
53
+
54
+ ## Not only visits...
55
+
56
+ Of course, you can use Trackoid to track all actions who require numeric analytics in a date frame.
57
+
58
+ ### Prevent login to a control panel with a maximum login attemps
59
+
60
+ You can track invalid logins so you can prevent login for a user when certain invalid login had been made. Imagine your login controller:
61
+
62
+ # User model
63
+ class User
64
+ include Mongoid::Document
65
+ include Mongoid::Tracking
66
+
67
+ track :failed_logins
68
+ end
69
+
70
+ # User controller
71
+ def login
72
+ user = User.find(params[:email])
73
+
74
+ # Stop login if failed attemps > 3
75
+ redirect(root_path) if user.failed_logins.today > 3
76
+
77
+ # Continue with the normal login steps
78
+ if user.authenticate(params[:password])
79
+ redirect_back_or_default(root_path)
80
+ else
81
+ user.failed_logins.inc
82
+ end
83
+ end
84
+
85
+ Note that additionally you have the full failed login history for free. :-)
86
+
87
+ # All failed login attemps, ever.
88
+ @user.failed_logins.sum
89
+
90
+ # Failed logins this month.
91
+ @user.failed_logins.this_month
92
+
93
+
94
+ ### Automatically saving a history of document changes
95
+
96
+ You can combine Trackoid with the power of callbacks to automatically track certain operations, for example modification of a document. This way you have a history of document changes.
97
+
98
+ class User
99
+ include Mongoid::Document
100
+ include Mongoid::Tracking
101
+
102
+ field :name
103
+ track :changes
104
+
105
+ after_update :track_changes
106
+
107
+ protected
108
+ def track_changes
109
+ self.changes.inc
110
+ end
111
+ end
112
+
113
+
114
+ ### Track temperature history for a nuclear plant
115
+
116
+ Imagine you need a web service to track the temperature of all rooms of a nuclear plant. Now you have a simple method to do this:
117
+
118
+ # Room temperature
119
+ class Room
120
+ include Mongoid::Document
121
+ include Mongoid::Tracking
122
+
123
+ track :temperature
124
+ end
125
+
126
+
127
+ # Temperature controller
128
+ def set_temperature_for_room
129
+ @room = Room.find(params[:room_number])
130
+
131
+ @room.temperature.set(current_temperature)
132
+ end
133
+
134
+ So, you are not restricted into incrementing or decrementing a value, you can also store an specific value. Now it's easy to know the maximum temperature of the last 30 days for a room:
135
+
136
+ @room.temperature.last_days(30).max
137
+
138
+
139
+ # How does it works?
140
+
141
+ Trakoid works by embedding date tracking information into the models. The date tracking information is limited by a granularity of days, but you can use aggregates if you absolutely need hour or minutes granularity.
142
+
143
+
144
+ ## Scalability and performance
145
+
146
+ Trackoid is made from the ground up to take advantage of the great scalability features of MongoDB. Trackoid uses "upsert" operations, bypassing Mongoid controllers so that it can be used in a distributed system without data loss. This is perfect for a cloud hosted SaaS application!
147
+
148
+ The problem with a distributed system for tracking analytical information is the atomicity of operations. Imagine you must increment visits information from several servers at the same time and how you would do it. With an SQL model, this is somewhat easy because the tradittional approaches for doing this only require INSERT or UPDATE operations that are atomic by nature. But for a Document Oriented Database like MongoDB you need some kind of special operations. MongoDB uses "upsert" commands, which stands for "update or insert". That is, modify this and create if not exists.
149
+
150
+ The problem with Mongoid, and with all other ORM for that matter, is that they are not made with those operations in mind. If you store an Array or Hash into a Mongoid document, you read or save it as a whole, you can not increment or store only a value without reading/writting the full Array.
151
+
152
+ Trackoid issues "upsert" commands directly to the MongoDB driver, with the following structure:
153
+
154
+
155
+ collection.update( {_id:ObjectID}, {$inc: {visits.2010.05.30: 1} }, true )
156
+
157
+
158
+ This way, the collection can receive multiple incremental operations without requiring additional logic for locking or something. The only drawback is that you will not have realtime data in your model. For example:
159
+
160
+ v = @page.visits.today # v is now "5" if there was 5 visits today
161
+ @page.visits.inc # Increment visits today
162
+ @page.visits.today == v+1 # Visits is now incremented in our local copy
163
+ # of the object, but we need to reload for it
164
+ # to reflect the realtime visits to the page
165
+ # since there could be another processes
166
+ # updating visits
167
+
168
+ In practice, we don't need visits information so fine grained, but it's good to take this into account.
169
+
170
+ ## Embedding tracking information into models
171
+
172
+ Tracking analytics data in SQL databases was historicaly saved into her own table, perhaps called `site_visits` with a relation to the sites table and each row saving an integer for each day.
173
+
174
+ Table "site_visits"
175
+
176
+ SiteID Date Visits
177
+ ------ ---------- ------
178
+ 1234 2010-05-01 34
179
+ 1234 2010-05-02 25
180
+ 1234 2010-05-03 45
181
+
182
+ With this schema, it's easy to get visits for a website using single SQL statements. However, for complex queries this can be easily become cumbersome. Also this doesn't work so well for systems using a generic SQL DSL like ActiveRecord since for really taking advantage of some queries you need to use SQL language directly, one option that isn't neither really interesting nor available.
183
+
184
+ Trackoid uses an embedding approach to tackle this. For the above examples, Trackoid would embedd a ruby Hash into the Site model. This means the tracking information is already saved "inside" the Site, and we don't have to reach the database for any date querying! Moreover, since the data retrieved with the accessor methods like "last_days", "this_month" and the like, are already arrays, we could use Array methods like sum, count, max, min, etc...
185
+
186
+ ## Memory implications
187
+
188
+ Since storing all tracking information with the model implies we add additional information that can grow, and grow, and grow... You can be wondering yourself if this is a good idea. Yes, it's is, or at least I think so. Let me convice you...
189
+
190
+ MongoDB stores information in BSON format as a binary representation of a JSON structure. So, BSON stores integers like integers, not like string representations of ASCII characters. This is important to calculate the space used for analytic information.
191
+
192
+ A year full of statistical data takes only 2.8Kb, if you store integers. If your statistical data includes floats, a year full of information takes 4.3Kb. I said "a year full of data" because Trackoid does not store information for days without data.
193
+
194
+ For comparison, this README is already 8.5Kb in size...
195
+
196
+
197
+ # Changes for TZ support
198
+
199
+ Well, this is the time (no pun intended) to add TZ support to Trackoid.
200
+
201
+ The problem is that "today" is not the same "today" for everyone, so unless you live in UTC or don't care about time zones, you probably should stay in 0.2.0 version and live long and prosper...
202
+
203
+ But... Okay, given the fact that "today" is not the same "today" for everyone, this is the brand new Trackoid, with TZ support.
204
+
205
+ ## What has changed?
206
+
207
+ In the surface, almost nothing, but internally there has been a major rewrite of the tracking code (the 'inc', 'set' methods) and the readers ('today', 'yesterday', etc). This is due to the changes I've made to the MongoDB structure of the tracking data.
208
+
209
+ <b>YOU WILL NEED TO MIGRATE THE EXISTING DATA IF YOU WANT TO KEEP IT</b>
210
+
211
+ This is very important, so I will repeat:
212
+
213
+ <b>YOU WILL NEED TO MIGRATE THE EXISTING DATA IF YOU WANT TO KEEP IT</b>
214
+
215
+ The internal JSON structure of a tracking field was like that.
216
+
217
+ {
218
+ ... some other fields in the model...,
219
+ "tracking_data" : {
220
+ "2011" : {
221
+ "01" : {
222
+ "01" : 10,
223
+ "02" : 20,
224
+ "03" : 30,
225
+ ...
226
+ },
227
+ "02" : {
228
+ "01" : 10,
229
+ "02" : 20,
230
+ "03" : 30,
231
+ ...
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ That is, years, months and days numbers created a nested hash in which the final data (leaves) was the amount tracked. You see? There was no trace of hours... That's the problem.
238
+
239
+ This is the new, TZ aware version of the internal JSON structure:
240
+
241
+ {
242
+ ... some other fields in the model...,
243
+ "tracking_data" : {
244
+ "14975" : {
245
+ "00" : 10,
246
+ "01" : 20,
247
+ "02" : 30,
248
+ ...
249
+ "22" : 88,
250
+ "23" : 99
251
+ },
252
+ "14976" : {
253
+ "00" : 10,
254
+ "01" : 20,
255
+ "02" : 30,
256
+ ...
257
+ "22" : 88,
258
+ "23" : 99
259
+ }
260
+ }
261
+ }
262
+
263
+ So, instead of a nested array with keys like year/month/day, I now use the timestamp of the date. Well, a cooked timestamp. "14975" is the numbers of days since the epoch, which is the number of seconds elapsed since midnight Coordinated Universal Time (UTC) of January 1, 1970, and blah, blah, blah... You know what's this all about (http://en.wikipedia.org/wiki/Unix_time)
264
+
265
+ The exact formula is like this (Ruby):
266
+
267
+ date_index = Time.now.utc.to_i / 60 / 60 / 24
268
+
269
+ The contents of every "day record" is another hash with 24 keys, one for each hour. This MUST be a hash, not an array (which might be more natural) sice Trackoid uses "upserts" operations to be atomic. Reading the array, modifying it and saving it back is not an option. The exact MongoDB operation to support this is as follows:
270
+
271
+ db.update(
272
+ { search_criteria },
273
+ { "$inc" => {"track_data.14976.10" => 1} },
274
+ { :upsert => true, :safe => false }
275
+ )
276
+
277
+ ## What "today" is it?
278
+
279
+ All dates are saved in UTC. That means Trackoid returns a whole 24 hour block for "today" only where the TZ is exactly UTC/GMT (no offset). If you live in a country where there is an offset into UTC, Trackoid must read a whole block and some hours from the block before or after to build "your today".
280
+
281
+ Example: I live in GMT+0200 (Daylight saving in effect, or summer time), then if I request data for "today" as of 2011-04-14, Trackoid must first read the block for 15078 (UTC index for 2011-04-14), shift up 2 hours and then fill the missing 2 hours from the day before (15078). The entire block will be like this:
282
+
283
+ "tracking_data" : {
284
+ "15078" : {
285
+ "22" : 88, # Last two hours from 2011-04-13 UTC
286
+ "23" : 99
287
+ },
288
+ "15079" : {
289
+ "00" : 10,
290
+ "01" : 20,
291
+ "02" : 30,
292
+ ""
293
+ }
294
+
295
+ This is a more graphical representation:
296
+
297
+ Hours 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23
298
+ ------ -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
299
+ GMT+2: 00 00 00 XX 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
300
+ UTC: ---> 00 XX 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
301
+ Shift up ---> 2 hours.
302
+
303
+
304
+ For timezones with a negative offset from UTC (Like PDT/PST) the process is reversed: UTC values are shifted down and holes filled with the following day.
305
+
306
+
307
+ ## How should I tell Trackoid how TZ to use?
308
+
309
+ Piece of cake: Use the reader methods "today", "yesterday", "last_days(N)" and Trackoid will use the effective Time Zone of your Rails/Ruby application.
310
+
311
+ Trackoid will correctly translate dates for you (hopefully) if you pass a date to any of those methods.
312
+
313
+
314
+
315
+ # Revision History
316
+
317
+ 0.1.1 - Corrected license.
318
+ 0.1.0 - Complete rewrite and fixes of the original trackoid gem to work with Mongoid 4.0.1
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
@@ -0,0 +1,6 @@
1
+ test:
2
+ sessions:
3
+ default:
4
+ database: trackoid_test
5
+ hosts:
6
+ - 127.0.0.1:27017
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Tracking #:nodoc:
4
+ module Aggregates
5
+
6
+ DEPRECATED_TOKENS = ['hour', 'hours']
7
+
8
+ # This module includes aggregate data extensions to Trackoid instances
9
+ def self.included(base)
10
+ base.class_eval do
11
+ extend ClassMethods
12
+
13
+ class_attribute :aggregate_fields, :aggregate_klass
14
+ self.aggregate_fields = {}
15
+ self.aggregate_klass = nil
16
+ delegate :aggregate_fields, :aggregate_klass, :aggregated?,
17
+ to: "self.class"
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ # Defines an aggregate token to an already tracked model. It defines
23
+ # a new mongoid model named after the original model.
24
+ #
25
+ # Example:
26
+ #
27
+ # <tt>class Page</tt>
28
+ # <tt> include Mongoid::Document</tt>
29
+ # <tt> include Mongoid::Tracking</tt>
30
+ # <tt> track :visits</tt>
31
+ # <tt> aggregate :browsers do |b|</tt>
32
+ # <tt> b.split(" ").first</tt>
33
+ # <tt> end</tt>
34
+ # <tt>end</tt>
35
+ #
36
+ # A new model is defined as <tt>class PageAggregates</tt>
37
+ #
38
+ # This model has the following structure:
39
+ #
40
+ # <tt>belongs_to :page</tt>
41
+ # <tt>field :ns, :type => String</tt>
42
+ # <tt>field :key, :type => String</tt>
43
+ # <tt>index [:page_id, :ns, :key], :unique => true</tt>
44
+ # <tt>track :[original_parent_tracking_data]</tt>
45
+ # <tt>track :...</tt>
46
+ #
47
+ # :ns is the "namespace". It's the name you put along the
48
+ # "aggregate :browsers" in the original model definition.
49
+ #
50
+ # :key is your aggregation key. This is the value you are required to
51
+ # return in the "aggregate" block.
52
+ #
53
+ # With the above structure, you can always query aggregates directly
54
+ # using Mongoid this way:
55
+ #
56
+ # <tt>TestModelAggregates.where(:ns => "browsers", :key => "explorer").first</tt>
57
+ #
58
+ # But you are encouraged to use Trackoid methods whenever possible.
59
+ #
60
+ def aggregate(name, &block)
61
+ raise Errors::AggregationAlreadyDefined.new(self.name, name) if aggregate_fields.has_key? name
62
+ raise Errors::AggregationNameDeprecated.new(name) if DEPRECATED_TOKENS.include? name.to_s
63
+
64
+ define_aggregate_model if aggregate_klass.nil?
65
+ has_many internal_accessor_name(name), class_name: aggregate_klass.to_s
66
+ add_aggregate_field(name, block)
67
+ create_aggregation_accessors(name)
68
+ end
69
+
70
+ # Return true if this model has aggregated data.
71
+ def aggregated?
72
+ !aggregate_klass.nil?
73
+ end
74
+
75
+ protected
76
+ # Returns the internal representation of the aggregates class name
77
+ def internal_aggregates_name
78
+ str = self.to_s.underscore + "_aggregates"
79
+ str.camelize
80
+ end
81
+
82
+ def internal_accessor_name(name)
83
+ (name.to_s + "_accessor").to_sym
84
+ end
85
+
86
+ # Defines the aggregation model. It checks for class name conflicts
87
+ def define_aggregate_model
88
+ unless defined?(Rails) && Rails.env.development?
89
+ raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined?
90
+ end
91
+
92
+ parent = self
93
+
94
+ define_klass do
95
+ include Mongoid::Document
96
+ include Mongoid::Tracking
97
+
98
+ # Make the relation to the original class
99
+ belongs_to parent.name.demodulize.underscore.to_sym, class_name: parent.name
100
+
101
+ # Internal fields to track aggregation token and keys
102
+ field :ns, type: String
103
+ field :key, type: String
104
+
105
+ index({
106
+ parent.name.foreign_key.to_sym => 1,
107
+ ns: 1,
108
+ key: 1
109
+ }, { unique: true, background: true })
110
+
111
+ # Include parent tracking data.
112
+ parent.tracked_fields.each { |track_field| track track_field }
113
+ end
114
+
115
+ self.aggregate_klass = internal_aggregates_name.constantize
116
+ end
117
+
118
+ # Returns true if there is a class defined with the same name as our
119
+ # aggregate class.
120
+ def foreign_class_defined?
121
+ internal_aggregates_name.constantize && true
122
+ rescue NameError
123
+ false
124
+ end
125
+
126
+ # Adds the aggregate field to the array of aggregated fields.
127
+ def add_aggregate_field(name, block)
128
+ aggregate_fields[name] = block
129
+ end
130
+
131
+ # Defines the aggregation external class. This class is named after
132
+ # the original class model but with "Aggregates" appended.
133
+ # Example: TestModel ==> TestModelAggregates
134
+ def define_klass(&block)
135
+ scope = internal_aggregates_name.split('::')
136
+ klass = scope.pop
137
+ scope = scope.inject(Object) do |scope, const_name|
138
+ scope.const_get(const_name)
139
+ end
140
+ klass = scope.const_set(klass, Class.new)
141
+ klass.class_eval(&block)
142
+ end
143
+
144
+ def create_aggregation_accessors(name)
145
+ # Aggregation accessors in the model acts like a named scopes
146
+ define_method(name) do |*args|
147
+ TrackerAggregates.new(self, name, args)
148
+ end
149
+
150
+ define_method("#{name}_with_track") do |track_field, *args|
151
+ TrackerAggregates.new(self, name, args, track_field)
152
+ end
153
+
154
+ define_method("#{name}=") do
155
+ raise NoMethodError
156
+ end
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,53 @@
1
+ # encoding: utf-8
2
+ class Range
3
+ # Adds some enumerable capabilities to Time ranges
4
+ # (Normally they raise a "Can't iterate time ranges")
5
+ #
6
+ # It works by assuming days while iterating the time range, but you can
7
+ # pass an optional parameter
8
+
9
+ HOURS = 3600
10
+ DAYS = 24*HOURS
11
+ DEFAULT_TIME_GRANULARITY = DAYS
12
+
13
+ # Map / Collect over a Time range.
14
+ # A better implementation would be redefining 'succ' on Time. However,
15
+ # the ruby source code (At least 1.9.2-p0) hardcodes a check for Type,
16
+ # so it would not work even if we provide our own 'succ' for Time.
17
+ def collect(step = DEFAULT_TIME_GRANULARITY)
18
+ return super() unless first.is_a?(Time)
19
+
20
+ return collect(step) {|c| c} unless block_given?
21
+
22
+ # Pretty much a standard implementation of Map/Collect here
23
+ ary, current, op = [], first, (exclude_end? ? :< : :<=)
24
+ while current.send(op, last)
25
+ ary << yield(current)
26
+ current = current + step
27
+ end
28
+ ary
29
+ end
30
+ alias :map :collect
31
+
32
+ # Diff returns the number of elements in the Range, much like 'count'.
33
+ # Again, redefining 'succ' would be a better idea (see above).
34
+ # However, I think redefining 'succ' makes this O(n) while this is O(1)
35
+ def diff(granularity = DEFAULT_TIME_GRANULARITY)
36
+ if first.is_a?(Time)
37
+ @diff ||= (last - first) / granularity + (exclude_end? ? 0 : 1)
38
+ @diff.to_i
39
+ else
40
+ @diff ||= count
41
+ end
42
+ end
43
+
44
+ # Helper methods for non default parameters
45
+ def hour_diff
46
+ diff(HOURS)
47
+ end
48
+
49
+ def hour_collect(&block)
50
+ collect(HOURS, &block)
51
+ end
52
+ alias :hour_map :hour_collect
53
+ end