calculon 0.0.1

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/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ docs
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create ruby-1.9.2-p0@calculon
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in calculon.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Brian Muller
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Calculon
2
+
3
+ Calculon provides aggregate time functions for ActiveRecord.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'calculon'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```
22
+ $ gem install calculon
23
+ ```
24
+
25
+ ## Usage
26
+ Calculon allows you to group attributes using aggregate functions (sum, avg, min, max, etc) into time buckets (minute, hour, day, month, year). These buckets are "calendar" size - for instance, "by hour" means between absolute clock hours rather than a relative "within last 60 minutes, between 60-120 minutes ago, etc."
27
+
28
+ Let's say you have a Game model with two columns, one for Team A's points and the other for Team B's points.
29
+
30
+ ```ruby
31
+ class Game
32
+ attr_accessible :team_a_points, :team_b_points
33
+ end
34
+ ```
35
+
36
+ And now you want to know the total points for both teams by day:
37
+
38
+ ```ruby
39
+ Game.by_day(:team_a_points => :sum, :team_b_points => :sum)
40
+ ```
41
+
42
+ Awesome. Now let's say you want to know the average yesterday where Team A scored more than 0 points:
43
+
44
+ ```ruby
45
+ Game.by_day(:team_a_points => :avg, :team_b_points => :avg).on(Date.yesterday).where('team_a_points > 0')
46
+ ```
47
+
48
+ Now say you hate typing, and want to get points more easily:
49
+
50
+ ```ruby
51
+ class Game
52
+ calculon_view :points, :team_a_points => :sum, :team_b_points => :sum
53
+ end
54
+ ```
55
+
56
+ Now, you can get point sums more naturally:
57
+
58
+ ```ruby
59
+ Game.points_by_day.on(Date.yesterday)
60
+ Game.points_by_month.where('team_a_points > 0')
61
+ Game.points_by_year
62
+ ```
63
+
64
+ 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
+
66
+ ```ruby
67
+ nogame = OpenStruct.new(:team_a_points => 0, :team_b_points => 0)
68
+ Game.points_by_hour.on(Date.yesterday).to_filled_a(nogame)
69
+ ```
70
+
71
+ This will return an array of length 24, with "nogame" filling in each hour for which there was no game.
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ Rake::TestTask.new("test") { |t|
6
+ t.libs += ["lib", "."]
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.verbose = true
9
+ }
10
+
11
+ task :default => [:test]
12
+
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
+ }
data/calculon.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'calculon/version'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "calculon"
7
+ gem.version = Calculon::VERSION
8
+ gem.authors = ["Brian Muller"]
9
+ gem.email = ["bamuller@gmail.com"]
10
+ gem.description = %q{Calculon provides aggregate time functions for ActiveRecord.}
11
+ gem.summary = %q{Calculon provides aggregate time functions for ActiveRecord.}
12
+ gem.homepage = "https://github.com/opbandit/calculon"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ["lib"]
18
+ gem.add_dependency("rails", ">= 3.2.0")
19
+ gem.add_development_dependency('rdoc')
20
+ gem.add_development_dependency('rake')
21
+ gem.add_development_dependency('sqlite3')
22
+ gem.add_development_dependency('mysql2')
23
+ end
data/lib/calculon.rb ADDED
@@ -0,0 +1,115 @@
1
+ require "calculon/version"
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
23
+
24
+ module Calculon
25
+ def self.included(base)
26
+ base.extend(ClassMethods)
27
+ end
28
+
29
+ module ClassMethods
30
+ def calculon_view(name, cols, opts=nil)
31
+ metaclass = class << self; self; end
32
+ metaclass.instance_eval do
33
+ [ "minute", "hour", "day", "month", "year" ].each do |window|
34
+ define_method("#{name}_by_#{window}") { |nopts=nil|
35
+ nopts = (opts || {}).merge(nopts || {})
36
+ send("by_#{window}".intern, cols, nopts)
37
+ }
38
+ end
39
+ end
40
+ end
41
+
42
+ def default_time_column(column)
43
+ @@calculon_time_column = column
44
+ end
45
+
46
+ def default_calculon_opts
47
+ @@calculon_time_column ||= "created_at"
48
+ { :time_column => @@calculon_time_column, :bycols => [] }
49
+ end
50
+
51
+ def on(date, opts=nil)
52
+ opts = default_calculon_opts.merge(opts || {})
53
+ raise "'on' method takes a Date object as the first param" unless date.is_a?(Date)
54
+ between date.to_time, date.to_time + 86399.seconds
55
+ end
56
+
57
+ def between(starttime, endtime, opts=nil)
58
+ opts = default_calculon_opts.merge(opts || {})
59
+ relation = where ["#{opts[:time_column]} >= ? and #{opts[:time_column]} <= ?", starttime, endtime]
60
+ relation.calculon_opts ||= {}
61
+ relation.calculon_opts.merge!(:starttime => starttime, :endtime => endtime)
62
+ relation
63
+ end
64
+
65
+ def by_minute(cols, opts=nil)
66
+ tcol = "concat(date(%{time_column}),' ',lpad(hour(%{time_column}),2,'0'),':',lpad(minute(%{time_column}),2,'0'),':00')"
67
+ by_bucket :minute, tcol, cols, opts
68
+ end
69
+
70
+ def by_hour(cols, opts=nil)
71
+ by_bucket :hour, "concat(date(%{time_column}),' ',lpad(hour(%{time_column}),2,'0'),':00:00')", cols, opts
72
+ end
73
+
74
+ def by_day(cols, opts=nil)
75
+ by_bucket :day, "concat(date(%{time_column}),' 00:00:00')", cols, opts
76
+ end
77
+
78
+ def by_month(cols, opts=nil)
79
+ by_bucket :month, "concat(year(%{time_column}),'-',lpad(month(%{time_column}),2,'0'),'-01 00:00:00')", cols, opts
80
+ end
81
+
82
+ def by_year(cols, opts=nil)
83
+ by_bucket :year, "concat(year(%{time_column}),'-01-01 00:00:00')", cols, opts
84
+ end
85
+
86
+ def by_bucket(bucket_name, bucket, cols, opts=nil)
87
+ opts = default_calculon_opts.merge(opts || {})
88
+
89
+ unless ActiveRecord::Base.connection.adapter_name == "Mysql2"
90
+ raise "Mysql2 is the only supported connection adapter for calculon"
91
+ end
92
+
93
+ # Set column in bucket string. This should be based on localtime, in case there are some points
94
+ # that fall on both sides of a date - so group by this conversion. The 'where' doesn't
95
+ # need this conversion because Rails does this automatically before calling the query
96
+ # (see http://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-default_timezone)
97
+ time_column = "CONVERT_TZ(#{opts[:time_column]}, '+00:00', '#{Time.zone.formatted_offset}')"
98
+ bucket = bucket % { :time_column => time_column }
99
+
100
+ # 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 }
103
+ cols = cols.map { |name,method|
104
+ asname = name.to_s.gsub(' ', '').tr('^A-Za-z0-9', '_')
105
+ method.nil? ? name : "#{method}(#{name}) as #{asname}"
106
+ } + [ "#{bucket} as time_bucket" ]
107
+
108
+ relation = select(cols.join(",")).group(*groupby).order("time_bucket ASC")
109
+ relation.calculon_opts ||= {}
110
+ relation.calculon_opts.merge!(opts)
111
+ relation.calculon_opts[:bybucket] = bucket_name
112
+ relation
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,11 @@
1
+ module Calculon
2
+
3
+ class Railtie < Rails::Railtie
4
+ initializer 'calculon.insert_into_active_record' do
5
+ ActiveSupport.on_load :active_record do
6
+ ActiveRecord::Base.send(:include, Calculon)
7
+ end
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,61 @@
1
+ module Calculon
2
+ class SummarizedResults < Hash
3
+ attr_reader :rows
4
+
5
+ def initialize(relation)
6
+ super({})
7
+
8
+ @bucket_size = relation.calculon_opts[:bybucket]
9
+ @grouped_by = relation.calculon_opts[:bycols] || []
10
+
11
+ @start_time = relation.calculon_opts[:starttime] || keys.sort.first
12
+ @start_time = @start_time.to_time if @start_time.is_a?(Date)
13
+
14
+ @end_time = relation.calculon_opts[:endtime] || keys.sort.last
15
+ @end_time = @end_time.to_time if @end_time.is_a?(Date)
16
+
17
+ relation.to_a.each { |row|
18
+ self[row.time_bucket] = row
19
+ }
20
+ end
21
+
22
+ def fill_missing!(value=nil)
23
+ each_time { |key|
24
+ self[key] = value unless has_key?(key)
25
+ }
26
+ self
27
+ end
28
+
29
+ def to_a
30
+ results = []
31
+ each_time { |key|
32
+ results << self[key] if self.has_key?(key)
33
+ }
34
+ results
35
+ end
36
+
37
+ def time_format
38
+ {
39
+ :minute => "%Y-%m-%d %H:%M:00",
40
+ :hour => "%Y-%m-%d %H:00:00",
41
+ :day => "%Y-%m-%d 00:00:00",
42
+ :month => "%Y-%m-01 00:00:00",
43
+ :year => "%Y-01-01 00:00:00"
44
+ }.fetch(@bucket_size)
45
+ end
46
+
47
+ def each_time
48
+ increment_amounts = { :minute => 1.minute, :hour => 1.hour, :day => 1.day, :month => 1.month, :year => 1.year }
49
+ increment = increment_amounts[@bucket_size]
50
+
51
+ # get the "floor" of the start and end times (the "floor" bucket)
52
+ current = Time.zone.parse(@start_time.strftime(time_format + " %z"))
53
+ last_time = Time.zone.parse(@end_time.strftime(time_format + " %z"))
54
+
55
+ while current <= last_time
56
+ yield current.strftime(time_format)
57
+ current += increment
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,3 @@
1
+ module Calculon
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,64 @@
1
+ require 'test/unit'
2
+ require 'rails/all'
3
+ require 'calculon'
4
+
5
+ ActiveRecord::Base.establish_connection adapter: "mysql2", database: "calculon_test", username: 'root'
6
+ ActiveRecord::Base.default_timezone = :utc
7
+ Time.zone = "UTC"
8
+
9
+ class Game < ActiveRecord::Base
10
+ include Calculon
11
+ calculon_view :points, :team_a_points => :sum, :team_b_points => :sum
12
+ end
13
+
14
+ class CalculonTest < Test::Unit::TestCase
15
+ def setup
16
+ old = $stdout
17
+ $stdout = StringIO.new
18
+ ActiveRecord::Base.logger
19
+ ActiveRecord::Schema.define(version: 1) do
20
+ create_table :games do |t|
21
+ t.column :team_a_points, :integer, default: 0
22
+ t.column :team_b_points, :integer, default: 0
23
+ t.timestamps
24
+ end
25
+ end
26
+ $stdout = old
27
+ end
28
+
29
+ def teardown
30
+ ActiveRecord::Base.connection.tables.each do |table|
31
+ ActiveRecord::Base.connection.drop_table(table)
32
+ end
33
+ end
34
+
35
+ def test_results_hash
36
+ Game.create(:team_a_points => 10, :team_b_points => 20, :created_at => 33.hours.ago)
37
+ Game.create(:team_a_points => 30, :team_b_points => 40, :created_at => 2.hours.ago)
38
+
39
+ assert_equal Game.by_hour(:team_a_points => :sum).length, 2
40
+ results = Game.points_by_hour.to_results_hash
41
+ keys = [33.hours.ago.strftime("%Y-%m-%d %H:00:00"), 2.hours.ago.strftime("%Y-%m-%d %H:00:00")]
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
45
+
46
+ assert_equal Game.by_day(:team_a_points => :sum).length, 2
47
+ results = Game.points_by_day.to_results_hash
48
+ keys = [33.hours.ago.strftime("%Y-%m-%d 00:00:00"), 2.hours.ago.strftime("%Y-%m-%d 00:00:00")]
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
52
+ end
53
+
54
+ def test_results_hash_missing
55
+ Game.create(:team_a_points => 10, :created_at => Time.zone.now - 0.hours)
56
+ Game.create(:team_a_points => 20, :created_at => Time.zone.now - 1.hours)
57
+ Game.create(:team_a_points => 30, :created_at => Time.zone.now - 2.hours)
58
+ Game.create(:team_a_points => 40, :created_at => Time.zone.now - 25.hours)
59
+
60
+ days = Game.points_by_day.to_a
61
+ assert_equal days.length, 2
62
+ assert_equal days.inject(0) { |s,g| s + g.team_a_points }, 100
63
+ end
64
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: calculon
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Brian Muller
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2013-04-21 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ requirement: &id001 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 3.2.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: rdoc
28
+ requirement: &id002 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: rake
39
+ requirement: &id003 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: sqlite3
50
+ requirement: &id004 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
60
+ name: mysql2
61
+ requirement: &id005 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: "0"
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *id005
70
+ description: Calculon provides aggregate time functions for ActiveRecord.
71
+ email:
72
+ - bamuller@gmail.com
73
+ executables: []
74
+
75
+ extensions: []
76
+
77
+ extra_rdoc_files: []
78
+
79
+ files:
80
+ - .gitignore
81
+ - .rvmrc
82
+ - Gemfile
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - calculon.gemspec
87
+ - lib/calculon.rb
88
+ - lib/calculon/railtie.rb
89
+ - lib/calculon/summarized_results.rb
90
+ - lib/calculon/version.rb
91
+ - test/calculon_test.rb
92
+ homepage: https://github.com/opbandit/calculon
93
+ licenses: []
94
+
95
+ post_install_message:
96
+ rdoc_options: []
97
+
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ hash: 611864359157079367
106
+ segments:
107
+ - 0
108
+ version: "0"
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ hash: 611864359157079367
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project:
121
+ rubygems_version: 1.8.24
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: Calculon provides aggregate time functions for ActiveRecord.
125
+ test_files:
126
+ - test/calculon_test.rb