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