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 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
- attr_accessible :team_a_points, :team_b_points
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
- Awesome. Now let's say you want to know the average yesterday where Team A scored more than 0 points:
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
- calculon_view :points, :team_a_points => :sum, :team_b_points => :sum
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
- Game.points_by_hour.on(Date.yesterday).to_filled_a(nogame)
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 'rdoc/task'
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
- RDoc::Task.new("doc") { |rdoc|
14
- rdoc.title = "Calculon - aggregate methods for models for Rails"
15
- rdoc.rdoc_dir = 'docs'
16
- rdoc.rdoc_files.include('README.md')
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
@@ -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('rdoc')
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')
@@ -1,25 +1,7 @@
1
1
  require "calculon/version"
2
2
  require 'calculon/railtie'
3
- require 'calculon/summarized_results'
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
- send("by_#{window}".intern, cols, nopts)
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, :bycols => [] }
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[:bycols] + ["time_bucket"]
102
- opts[:bycols].each { |c| cols[c] = nil }
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}"
@@ -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 SummarizedResults < Hash
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[:bycols] || []
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
- self[row.time_bucket] = row
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 fill_missing!(value=nil)
23
- each_time { |key|
24
- self[key] = value unless has_key?(key)
25
- }
26
- self
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 to_a
30
- results = []
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 each_time
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 current.strftime(time_format)
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
@@ -1,3 +1,3 @@
1
1
  module Calculon
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -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.to_results_hash
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.to_results_hash
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.1
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-21 00:00:00 Z
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: rdoc
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: rake
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: sqlite3
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: mysql2
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/summarized_results.rb
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: 611864359157079367
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: 611864359157079367
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: