calculon 0.0.1 → 0.0.2
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 +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:
|