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 +18 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +18 -0
- data/calculon.gemspec +23 -0
- data/lib/calculon.rb +115 -0
- data/lib/calculon/railtie.rb +11 -0
- data/lib/calculon/summarized_results.rb +61 -0
- data/lib/calculon/version.rb +3 -0
- data/test/calculon_test.rb +64 -0
- metadata +126 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create ruby-1.9.2-p0@calculon
|
data/Gemfile
ADDED
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,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,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
|