calculon 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +41 -4
- data/Rakefile +9 -7
- data/calculon.gemspec +2 -1
- data/lib/calculon.rb +9 -24
- data/lib/calculon/ext.rb +14 -0
- data/lib/calculon/{summarized_results.rb → results.rb} +44 -17
- data/lib/calculon/version.rb +1 -1
- data/test/calculon_test.rb +6 -6
- metadata +22 -9
data/README.md
CHANGED
@@ -29,7 +29,7 @@ Let's say you have a Game model with two columns, one for Team A's points and th
|
|
29
29
|
|
30
30
|
```ruby
|
31
31
|
class Game
|
32
|
-
|
32
|
+
attr_accessible :team_a_points, :team_b_points
|
33
33
|
end
|
34
34
|
```
|
35
35
|
|
@@ -39,7 +39,9 @@ And now you want to know the total points for both teams by day:
|
|
39
39
|
Game.by_day(:team_a_points => :sum, :team_b_points => :sum)
|
40
40
|
```
|
41
41
|
|
42
|
-
|
42
|
+
This will return an array of Game instances where team_a_points and team_b_points are the sums per hour (the attribute time_bucket will give you the name of each bucket).
|
43
|
+
|
44
|
+
Now let's say you want to know the average yesterday where Team A scored more than 0 points:
|
43
45
|
|
44
46
|
```ruby
|
45
47
|
Game.by_day(:team_a_points => :avg, :team_b_points => :avg).on(Date.yesterday).where('team_a_points > 0')
|
@@ -49,7 +51,7 @@ Now say you hate typing, and want to get points more easily:
|
|
49
51
|
|
50
52
|
```ruby
|
51
53
|
class Game
|
52
|
-
|
54
|
+
calculon_view :points, :team_a_points => :sum, :team_b_points => :sum
|
53
55
|
end
|
54
56
|
```
|
55
57
|
|
@@ -64,8 +66,43 @@ Game.points_by_year
|
|
64
66
|
Let's say, however, that you want to know points by hour, but you want to get 24 results, regardless of whether or not a team scored (i.e., you want to fill in the "missing" hours):
|
65
67
|
|
66
68
|
```ruby
|
69
|
+
# first, get the buckets out
|
70
|
+
buckets = Game.points_by_hour.on(Date.yesterday).to_buckets
|
71
|
+
|
72
|
+
# then, convert to array with filled values
|
67
73
|
nogame = OpenStruct.new(:team_a_points => 0, :team_b_points => 0)
|
68
|
-
|
74
|
+
buckets.to_a(nogame)
|
69
75
|
```
|
70
76
|
|
71
77
|
This will return an array of length 24, with "nogame" filling in each hour for which there was no game.
|
78
|
+
|
79
|
+
### Multiple Grouping Columns
|
80
|
+
If you want to be able to group by other columns per time period, you can do that as well.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# the :group_by option takes either a single additional column or an array of columns
|
84
|
+
buckets = Game.points_by_hour(:group_by => :bracket_id).on(Date.yesterday).to_buckets
|
85
|
+
|
86
|
+
# see all of the unique grouping values
|
87
|
+
buckets.groupings
|
88
|
+
#=> { :bracket_id => 1, :bracket_id => 2 }
|
89
|
+
|
90
|
+
# get all of the time values for just bracket_id 1. Since this was by day, there will
|
91
|
+
# be 24 of them
|
92
|
+
buckets.values_for(:bracket_id => 1)
|
93
|
+
#=> [ #<Game ...>, #<Game ...>, ... ]
|
94
|
+
|
95
|
+
buckets.values_for(:bracket_id => 2)
|
96
|
+
#=> [ #<Game ...>, #<Game ...>, ... ]
|
97
|
+
|
98
|
+
# now, try one that doesn't exist
|
99
|
+
buckets.values_for(:bracket_id => 100)
|
100
|
+
#=> [ nil, nil, ... ]
|
101
|
+
|
102
|
+
# now, try one that doesn't exist, but w/ default value
|
103
|
+
buckets.values_for({:bracket_id => 100}, OpenStruct.new(:points => 0))
|
104
|
+
#=> [ #<OpenStruct points=0>, #<OpenStruct points=0>, ... ]
|
105
|
+
```
|
106
|
+
|
107
|
+
## Supported Databases
|
108
|
+
Right now, mysql2 is the only supported DB interface supported.
|
data/Rakefile
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
1
4
|
require 'bundler/gem_tasks'
|
2
5
|
require 'rake/testtask'
|
3
|
-
require '
|
6
|
+
require 'yard'
|
7
|
+
require 'calculon/version'
|
4
8
|
|
5
9
|
Rake::TestTask.new("test") { |t|
|
6
10
|
t.libs += ["lib", "."]
|
@@ -10,9 +14,7 @@ Rake::TestTask.new("test") { |t|
|
|
10
14
|
|
11
15
|
task :default => [:test]
|
12
16
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
18
|
-
}
|
17
|
+
YARD::Rake::YardocTask.new(:doc) do |task|
|
18
|
+
task.files = FileList["lib/**/*.rb"]
|
19
|
+
task.options = "--output", "docs", "--title", "Calculon #{Calculon::VERSION}", "--main", "README.md"
|
20
|
+
end
|
data/calculon.gemspec
CHANGED
@@ -16,7 +16,8 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
17
|
gem.require_paths = ["lib"]
|
18
18
|
gem.add_dependency("rails", ">= 3.2.0")
|
19
|
-
gem.add_development_dependency('
|
19
|
+
gem.add_development_dependency('yard')
|
20
|
+
gem.add_development_dependency('redcarpet')
|
20
21
|
gem.add_development_dependency('rake')
|
21
22
|
gem.add_development_dependency('sqlite3')
|
22
23
|
gem.add_development_dependency('mysql2')
|
data/lib/calculon.rb
CHANGED
@@ -1,25 +1,7 @@
|
|
1
1
|
require "calculon/version"
|
2
2
|
require 'calculon/railtie'
|
3
|
-
require 'calculon/
|
4
|
-
|
5
|
-
module ActiveRecord
|
6
|
-
module QueryMethods
|
7
|
-
attr_accessor :calculon_opts
|
8
|
-
end
|
9
|
-
|
10
|
-
class Relation
|
11
|
-
def to_results_hash
|
12
|
-
if(calculon_opts.nil?)
|
13
|
-
raise "Not a Calculon Relation: You must call by_day/by_hour/etc before you can call to_hash"
|
14
|
-
end
|
15
|
-
Calculon::SummarizedResults.new(self)
|
16
|
-
end
|
17
|
-
|
18
|
-
def to_filled_a(default=nil)
|
19
|
-
to_results_hash.fill_missing!(default).to_a
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
3
|
+
require 'calculon/results'
|
4
|
+
require 'calculon/ext'
|
23
5
|
|
24
6
|
module Calculon
|
25
7
|
def self.included(base)
|
@@ -33,7 +15,8 @@ module Calculon
|
|
33
15
|
[ "minute", "hour", "day", "month", "year" ].each do |window|
|
34
16
|
define_method("#{name}_by_#{window}") { |nopts=nil|
|
35
17
|
nopts = (opts || {}).merge(nopts || {})
|
36
|
-
|
18
|
+
# clone cols so modifications downstream don't affect our original copy here
|
19
|
+
send("by_#{window}".intern, cols.clone, nopts)
|
37
20
|
}
|
38
21
|
end
|
39
22
|
end
|
@@ -45,7 +28,7 @@ module Calculon
|
|
45
28
|
|
46
29
|
def default_calculon_opts
|
47
30
|
@@calculon_time_column ||= "created_at"
|
48
|
-
{ :time_column => @@calculon_time_column, :
|
31
|
+
{ :time_column => @@calculon_time_column, :group_by => [] }
|
49
32
|
end
|
50
33
|
|
51
34
|
def on(date, opts=nil)
|
@@ -85,6 +68,8 @@ module Calculon
|
|
85
68
|
|
86
69
|
def by_bucket(bucket_name, bucket, cols, opts=nil)
|
87
70
|
opts = default_calculon_opts.merge(opts || {})
|
71
|
+
# allow group by to be either single symbol or array of symbols
|
72
|
+
opts[:group_by] = [opts[:group_by]].flatten
|
88
73
|
|
89
74
|
unless ActiveRecord::Base.connection.adapter_name == "Mysql2"
|
90
75
|
raise "Mysql2 is the only supported connection adapter for calculon"
|
@@ -98,8 +83,8 @@ module Calculon
|
|
98
83
|
bucket = bucket % { :time_column => time_column }
|
99
84
|
|
100
85
|
# if we're grouping by other columns, we need to select them
|
101
|
-
groupby = opts[:
|
102
|
-
opts[:
|
86
|
+
groupby = opts[:group_by] + ["time_bucket"]
|
87
|
+
opts[:group_by].each { |c| cols[c] = nil }
|
103
88
|
cols = cols.map { |name,method|
|
104
89
|
asname = name.to_s.gsub(' ', '').tr('^A-Za-z0-9', '_')
|
105
90
|
method.nil? ? name : "#{method}(#{name}) as #{asname}"
|
data/lib/calculon/ext.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module QueryMethods
|
3
|
+
attr_accessor :calculon_opts
|
4
|
+
end
|
5
|
+
|
6
|
+
class Relation
|
7
|
+
def to_buckets
|
8
|
+
if(calculon_opts.nil?)
|
9
|
+
raise "Not a Calculon Relation: You must call by_day/by_hour/etc before you can call to_buckets"
|
10
|
+
end
|
11
|
+
Calculon::Results.create(self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,12 +1,15 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module Calculon
|
2
|
-
class
|
4
|
+
class Results < Hash
|
3
5
|
attr_reader :rows
|
4
6
|
|
5
7
|
def initialize(relation)
|
6
8
|
super({})
|
7
9
|
|
8
10
|
@bucket_size = relation.calculon_opts[:bybucket]
|
9
|
-
@grouped_by = relation.calculon_opts[:
|
11
|
+
@grouped_by = relation.calculon_opts[:group_by] || []
|
12
|
+
@grouped_by_values = Set.new
|
10
13
|
|
11
14
|
@start_time = relation.calculon_opts[:starttime] || keys.sort.first
|
12
15
|
@start_time = @start_time.to_time if @start_time.is_a?(Date)
|
@@ -15,23 +18,22 @@ module Calculon
|
|
15
18
|
@end_time = @end_time.to_time if @end_time.is_a?(Date)
|
16
19
|
|
17
20
|
relation.to_a.each { |row|
|
18
|
-
|
21
|
+
# Keep track of all of the unique column values for the group_by cols
|
22
|
+
@grouped_by_values.add @grouped_by.inject({}) { |h,col| h[col] = row.send(col); h }
|
23
|
+
self[row.time_bucket] = fetch(row.time_bucket, []) + [ row ]
|
19
24
|
}
|
20
25
|
end
|
21
|
-
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
|
27
|
+
def self.create(relation)
|
28
|
+
if (relation.calculon_opts[:group_by] || []).length > 0
|
29
|
+
MultiGroupingResults.new(relation)
|
30
|
+
else
|
31
|
+
SingleGroupingResults.new(relation)
|
32
|
+
end
|
27
33
|
end
|
28
34
|
|
29
|
-
def
|
30
|
-
|
31
|
-
each_time { |key|
|
32
|
-
results << self[key] if self.has_key?(key)
|
33
|
-
}
|
34
|
-
results
|
35
|
+
def groupings
|
36
|
+
@grouped_by_values.to_a
|
35
37
|
end
|
36
38
|
|
37
39
|
def time_format
|
@@ -44,7 +46,7 @@ module Calculon
|
|
44
46
|
}.fetch(@bucket_size)
|
45
47
|
end
|
46
48
|
|
47
|
-
def
|
49
|
+
def map_each_time
|
48
50
|
increment_amounts = { :minute => 1.minute, :hour => 1.hour, :day => 1.day, :month => 1.month, :year => 1.year }
|
49
51
|
increment = increment_amounts[@bucket_size]
|
50
52
|
|
@@ -52,10 +54,35 @@ module Calculon
|
|
52
54
|
current = Time.zone.parse(@start_time.strftime(time_format + " %z"))
|
53
55
|
last_time = Time.zone.parse(@end_time.strftime(time_format + " %z"))
|
54
56
|
|
57
|
+
results = []
|
55
58
|
while current <= last_time
|
56
|
-
yield
|
59
|
+
results << yield(current.strftime(time_format))
|
57
60
|
current += increment
|
58
61
|
end
|
62
|
+
results
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class SingleGroupingResults < Results
|
67
|
+
def to_a(default=nil)
|
68
|
+
map_each_time { |key|
|
69
|
+
fetch(key, [default]).first
|
70
|
+
}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class MultiGroupingResults < Results
|
75
|
+
def to_a
|
76
|
+
map_each_time { |key|
|
77
|
+
fetch(key, [])
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def values_for(grouping, default=nil)
|
82
|
+
map_each_time { |key|
|
83
|
+
matches = fetch(key, []).select { |value| grouping.map { |k,v| value.send(k) == v }.all? }
|
84
|
+
matches.length > 0 ? matches.first : default
|
85
|
+
}
|
59
86
|
end
|
60
87
|
end
|
61
88
|
end
|
data/lib/calculon/version.rb
CHANGED
data/test/calculon_test.rb
CHANGED
@@ -37,18 +37,18 @@ class CalculonTest < Test::Unit::TestCase
|
|
37
37
|
Game.create(:team_a_points => 30, :team_b_points => 40, :created_at => 2.hours.ago)
|
38
38
|
|
39
39
|
assert_equal Game.by_hour(:team_a_points => :sum).length, 2
|
40
|
-
results = Game.points_by_hour.
|
40
|
+
results = Game.points_by_hour.to_buckets
|
41
41
|
keys = [33.hours.ago.strftime("%Y-%m-%d %H:00:00"), 2.hours.ago.strftime("%Y-%m-%d %H:00:00")]
|
42
42
|
assert_equal keys, results.keys.sort
|
43
|
-
assert_equal results[keys.first].team_a_points, 10
|
44
|
-
assert_equal results[keys.last].team_b_points, 40
|
43
|
+
assert_equal results[keys.first].first.team_a_points, 10
|
44
|
+
assert_equal results[keys.last].first.team_b_points, 40
|
45
45
|
|
46
46
|
assert_equal Game.by_day(:team_a_points => :sum).length, 2
|
47
|
-
results = Game.points_by_day.
|
47
|
+
results = Game.points_by_day.to_buckets
|
48
48
|
keys = [33.hours.ago.strftime("%Y-%m-%d 00:00:00"), 2.hours.ago.strftime("%Y-%m-%d 00:00:00")]
|
49
49
|
assert_equal keys, results.keys.sort
|
50
|
-
assert_equal results[keys.first].team_a_points, 10
|
51
|
-
assert_equal results[keys.last].team_b_points, 40
|
50
|
+
assert_equal results[keys.first].first.team_a_points, 10
|
51
|
+
assert_equal results[keys.last].first.team_b_points, 40
|
52
52
|
end
|
53
53
|
|
54
54
|
def test_results_hash_missing
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: calculon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Brian Muller
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2013-04
|
13
|
+
date: 2013-05-04 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
@@ -24,7 +24,7 @@ dependencies:
|
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: *id001
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: yard
|
28
28
|
requirement: &id002 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
@@ -35,7 +35,7 @@ dependencies:
|
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: *id002
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
|
-
name:
|
38
|
+
name: redcarpet
|
39
39
|
requirement: &id003 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
@@ -46,7 +46,7 @@ dependencies:
|
|
46
46
|
prerelease: false
|
47
47
|
version_requirements: *id003
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
|
-
name:
|
49
|
+
name: rake
|
50
50
|
requirement: &id004 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
@@ -57,7 +57,7 @@ dependencies:
|
|
57
57
|
prerelease: false
|
58
58
|
version_requirements: *id004
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
|
-
name:
|
60
|
+
name: sqlite3
|
61
61
|
requirement: &id005 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
@@ -67,6 +67,17 @@ dependencies:
|
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
69
|
version_requirements: *id005
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: mysql2
|
72
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: "0"
|
78
|
+
type: :development
|
79
|
+
prerelease: false
|
80
|
+
version_requirements: *id006
|
70
81
|
description: Calculon provides aggregate time functions for ActiveRecord.
|
71
82
|
email:
|
72
83
|
- bamuller@gmail.com
|
@@ -85,8 +96,9 @@ files:
|
|
85
96
|
- Rakefile
|
86
97
|
- calculon.gemspec
|
87
98
|
- lib/calculon.rb
|
99
|
+
- lib/calculon/ext.rb
|
88
100
|
- lib/calculon/railtie.rb
|
89
|
-
- lib/calculon/
|
101
|
+
- lib/calculon/results.rb
|
90
102
|
- lib/calculon/version.rb
|
91
103
|
- test/calculon_test.rb
|
92
104
|
homepage: https://github.com/opbandit/calculon
|
@@ -102,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
102
114
|
requirements:
|
103
115
|
- - ">="
|
104
116
|
- !ruby/object:Gem::Version
|
105
|
-
hash:
|
117
|
+
hash: -4406503514097709404
|
106
118
|
segments:
|
107
119
|
- 0
|
108
120
|
version: "0"
|
@@ -111,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
123
|
requirements:
|
112
124
|
- - ">="
|
113
125
|
- !ruby/object:Gem::Version
|
114
|
-
hash:
|
126
|
+
hash: -4406503514097709404
|
115
127
|
segments:
|
116
128
|
- 0
|
117
129
|
version: "0"
|
@@ -124,3 +136,4 @@ specification_version: 3
|
|
124
136
|
summary: Calculon provides aggregate time functions for ActiveRecord.
|
125
137
|
test_files:
|
126
138
|
- test/calculon_test.rb
|
139
|
+
has_rdoc:
|