counter 0.0.5

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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2010 The New York Times Company
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this library except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,82 @@
1
+ = counter
2
+
3
+ Count things and rank them, as a one-off or over time.
4
+
5
+ Major components:
6
+ * Counter - a simple class to track counts, then sort and rank
7
+ * MovingCount - a base class built atop ActiveRecord to aggregate counts over time (think {rrdtool}[http://oss.oetiker.ch/rrdtool/] for counts)
8
+
9
+
10
+ === Getting started
11
+
12
+ gem install counter
13
+ config.gem 'counter'
14
+
15
+ require 'counter'
16
+
17
+ To use MovingCount, you'll also need to
18
+ require 'counter/moving_count'
19
+
20
+ (separate to prevent ActiveRecord dependency pollution)
21
+
22
+ === Counter
23
+
24
+ c = Counter.new
25
+
26
+ # Counts can be incremented as a key appears
27
+ c.increment('some-key')
28
+ c.increment('some-key')
29
+
30
+ # Or set directly
31
+ c.set('another-key', 42)
32
+
33
+ # You can retrieve all
34
+ c.counts
35
+ => [['another-key',42],['some-key',2]]
36
+
37
+ # Or just the top-n
38
+ c.top(1)
39
+ => [['another-key',42]]
40
+
41
+ See Counter docs for detail.
42
+
43
+ === MovingCount
44
+
45
+ require 'counter/moving_count'
46
+ class PageView < MovingCount
47
+ end
48
+
49
+ # In a migration:
50
+ # create_table :page_views, :force => true do |t|
51
+ # t.string :category, :null => false
52
+ # t.integer :count, :null => false, :default => 0
53
+ # t.datetime :sample_time, :null => false
54
+ # end
55
+ #
56
+ # add_index :page_views, :category
57
+
58
+ # First set of samples...
59
+ PageView.record_counts(Time.now - 5.minutes) do |c|
60
+ c.increment('a-key')
61
+ c.increment('a-key')
62
+ end
63
+
64
+ # Second set...
65
+ PageView.record_counts(Time.now) do |c|
66
+ c.increment('a-key')
67
+ c.increment('another-key')
68
+ end
69
+
70
+ # Contribute to totals.
71
+ PageView.totals
72
+ => [['a-key',3],['another-key',1]]
73
+
74
+ See MovingCount docs for detail.
75
+
76
+ == Author
77
+
78
+ Ben Koski, bkoski@nytimes.com
79
+
80
+ == Copyright
81
+
82
+ Copyright (c) 2010 The New York Times Company. See LICENSE for details.
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "counter"
8
+ gem.summary = %Q{count things, either as a one-off or aggregated over time}
9
+ gem.description = %Q{count things, either as a one-off or aggregated over time}
10
+ gem.email = "bkoski@nytimes.com"
11
+ gem.homepage = "http://github.com/nytimes/counter"
12
+ gem.authors = ["Ben Koski"]
13
+ gem.add_development_dependency "thoughtbot-shoulda"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/*_test.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ if File.exist?('VERSION')
48
+ version = File.read('VERSION')
49
+ else
50
+ version = ""
51
+ end
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "counter #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.5
@@ -0,0 +1,59 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{counter}
8
+ s.version = "0.0.5"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Ben Koski"]
12
+ s.date = %q{2010-07-29}
13
+ s.description = %q{count things, either as a one-off or aggregated over time}
14
+ s.email = %q{bkoski@nytimes.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "counter.gemspec",
27
+ "lib/counter.rb",
28
+ "lib/counter/counter.rb",
29
+ "lib/counter/moving_count.rb",
30
+ "test/schema.rb",
31
+ "test/test_counter.rb",
32
+ "test/test_helper.rb",
33
+ "test/test_moving_count.rb"
34
+ ]
35
+ s.homepage = %q{http://github.com/nytimes/counter}
36
+ s.rdoc_options = ["--charset=UTF-8"]
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.5}
39
+ s.summary = %q{count things, either as a one-off or aggregated over time}
40
+ s.test_files = [
41
+ "test/schema.rb",
42
+ "test/test_counter.rb",
43
+ "test/test_helper.rb",
44
+ "test/test_moving_count.rb"
45
+ ]
46
+
47
+ if s.respond_to? :specification_version then
48
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
49
+ s.specification_version = 3
50
+
51
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
52
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
55
+ end
56
+ else
57
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
58
+ end
59
+ end
@@ -0,0 +1 @@
1
+ require 'counter/counter'
@@ -0,0 +1,76 @@
1
+ # Basic counting.
2
+ #
3
+ # === To count:
4
+ #
5
+ # Easiest way to count is to increment while iterating over data:
6
+ #
7
+ # c = Counter.new
8
+ # c.increment('some-key')
9
+ # c.saw('some-key') # alias
10
+ #
11
+ # But you can also set counts directly:
12
+ # c = Counter.new
13
+ # c.set('another-key', 42)
14
+ #
15
+ # === To get counts back:
16
+ #
17
+ # You can use Hash-style syntax:
18
+ # puts c['some-key]
19
+ # => 2
20
+ #
21
+ # or ask for the top-n keys as an array of [key, count]
22
+ #
23
+ # puts c.top(2)
24
+ # => [['key-1',50],['key-2',38]]
25
+ #
26
+ # or just ask for data for all keys:
27
+ # puts c.counts
28
+ # => [['key-1',50],['key-2',38],['key-3',22]]
29
+ #
30
+ # === Some notes
31
+ # * 0 is returned, even if key has never been seen
32
+ # * if ties exist in a top-n ranking, they are broken by sort on key
33
+ class Counter
34
+
35
+ def initialize
36
+ @data = Hash.new(0)
37
+ end
38
+
39
+ # key++
40
+ def increment key
41
+ @data[key] += 1
42
+ true
43
+ end
44
+ alias_method :saw, :increment
45
+
46
+ # sets the key to a specified value
47
+ def set key, count
48
+ raise ArgumentError, "count must be numeric" if !count.is_a?(Numeric)
49
+ @data[key] = count
50
+ end
51
+
52
+ # Retrieve the count for a particular key
53
+ def [] key
54
+ @data[key]
55
+ end
56
+
57
+ # Returns all elements as [key, count] array
58
+ def counts
59
+ sorted_data
60
+ end
61
+
62
+ # Returns top <tt>n</tt> elements as [key,count] array
63
+ def top n
64
+ sorted_data[0,n]
65
+ end
66
+
67
+ def to_a
68
+ @data.to_a
69
+ end
70
+
71
+ private
72
+ def sorted_data
73
+ @data.to_a.sort { |a,b| a[1] != b[1] ? a[1] <=> b[1] : b[0] <=> a[0] }.reverse
74
+ end
75
+
76
+ end
@@ -0,0 +1,105 @@
1
+ require 'active_record'
2
+
3
+ # Capture counts, then aggregate them across time. Think of this as an rrdtool for counts.
4
+ #
5
+ # === Setup
6
+ # Inherit from this class
7
+ #
8
+ # class PageView < MovingCount
9
+ # end
10
+ #
11
+ # Then drop the following into a migration:
12
+ #
13
+ # create_table :page_views, :force => true do |t|
14
+ # t.string :category, :null => false
15
+ # t.integer :count, :null => false, :default => 0
16
+ # t.datetime :sample_time, :null => false
17
+ # end
18
+ #
19
+ # add_index :page_views, :category
20
+ #
21
+ # Two optional class-level settings:
22
+ # * *set_history_to_keep*, depth of history to keep in seconds - defaults to 1 hour of history
23
+ # * *set_sample_interval*, minimum distance between samples in seconds (enforced at record time) - defaults to 1 minute
24
+ #
25
+ # === Recording counts
26
+ #
27
+ # PageView.record_counts(Time.at(1280420630)) do |c|
28
+ # c.increment('key1')
29
+ # c.increment('key1')
30
+ # c.increment('key2')
31
+ # end
32
+ #
33
+ # records observed counts for Thu Jul 29 12:23:50. You can omit the param to record counts at Time.now.
34
+ #
35
+ # A runtime error will be raised if the sample is too close to the previous (for example, sample_interval is 60s and you tried to record
36
+ # a sample at 50s). This prevents accidental duping of counts on restarts, etc.
37
+ #
38
+ # === Checking totals
39
+ #
40
+ # PageView.totals
41
+ #
42
+ # returns a [key,count] array of all keys ordered by count. An optional <tt>limit</tt> param restricts results to the top n.
43
+ class MovingCount < ActiveRecord::Base
44
+ self.abstract_class = true
45
+
46
+ # Sets the depth of history to keep, in seconds.
47
+ # Default is 1 hour.
48
+ def self.set_history_to_keep time_range
49
+ @history_to_keep = time_range
50
+ end
51
+
52
+ def self.history_to_keep
53
+ @history_to_keep || 1.hour
54
+ end
55
+
56
+ # Sets the minimum distance between recorded samples, in seconds.
57
+ # Default is 1 minute.
58
+ def self.set_sample_interval interval
59
+ @sample_interval = interval
60
+ end
61
+
62
+ def self.sample_interval
63
+ @sample_interval || 1.minute
64
+ end
65
+
66
+ # Records counts at the specified timestamp. If timestamp omitted, counts are recorded at Time.now.
67
+ # Yields a Counter instance, all standard methods apply.
68
+ # Raises an exception if the timestamp would fall too soon under the <tt>sample_interval</tt>.
69
+ def self.record_counts timestamp=Time.now, &block
70
+ c = Counter.new
71
+ yield(c)
72
+
73
+ self.transaction do
74
+ check_sample_valid(timestamp)
75
+
76
+ q = "INSERT INTO #{self.table_name} (sample_time, category, count) VALUES "
77
+ c.counts.each { |key, count| q += self.sanitize_sql(['(?, ?, ?),', timestamp, key, count]) }
78
+ q.chop!
79
+
80
+ self.connection.execute(q)
81
+ self.delete_all(['sample_time < ?', (timestamp - history_to_keep)])
82
+ end
83
+
84
+ true
85
+ end
86
+
87
+ # Returns totals across entire history. Optional limit param restricts resultset.
88
+ def self.totals limit=nil
89
+ values = self.connection.select_rows("SELECT category, SUM(count) AS cnt FROM #{self.table_name} GROUP BY category ORDER BY cnt DESC #{"LIMIT #{limit}" if limit};")
90
+ values.map { |v| v[1] = v[1].to_i }
91
+ return values
92
+ end
93
+
94
+ private
95
+ def self.check_sample_valid timestamp
96
+ latest_sample = self.maximum(:sample_time)
97
+ return if latest_sample.nil? # sample is of course valid if no data exists
98
+
99
+ distance = (timestamp - latest_sample).abs
100
+ if distance < sample_interval
101
+ raise "Data recorded at #{self.maximum(:sample_time)} making #{timestamp} #{'%0.2f' % (sample_interval - distance)}s too soon on a #{sample_interval} interval."
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,18 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+
3
+ create_table :page_views, :force => true do |t|
4
+ t.string :category, :null => false
5
+ t.integer :count, :null => false, :default => 0
6
+ t.datetime :sample_time, :null => false
7
+ end
8
+
9
+ add_index :page_views, :category
10
+
11
+ create_table :clicks, :force => true do |t|
12
+ t.string :category, :null => false
13
+ t.integer :count, :null => false, :default => 0
14
+ t.datetime :sample_time, :null => false
15
+ end
16
+
17
+
18
+ end
@@ -0,0 +1,50 @@
1
+ require 'test_helper'
2
+
3
+ class CounterTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @c = Counter.new
7
+ end
8
+
9
+ should "increment count every time increment is called for a key" do
10
+ @c.increment('some-key')
11
+ @c.increment('some-key')
12
+ assert_equal 2, @c['some-key']
13
+ end
14
+
15
+ should "return 0 even if key has never been incremented" do
16
+ assert_equal 0, @c['some-key']
17
+ end
18
+
19
+ should "return count directly if it was set" do
20
+ @c.set('some-key', 65)
21
+ assert_equal 65, @c['some-key']
22
+ end
23
+
24
+ should "raise an ArgumentError if set called with a non-numeric" do
25
+ assert_raises(ArgumentError) { @c.set('some-key', Time.now) }
26
+ end
27
+
28
+ should "return counts as a [[key,cnt]] array" do
29
+ @c.set('some-key', 32)
30
+ @c.set('another-key', 45)
31
+
32
+ assert_equal [['another-key',45],['some-key',32]], @c.counts
33
+ end
34
+
35
+ should "return top(n) as a [[key,cnt]] array of top n objects based on count" do
36
+ @c.set('some-key', 32)
37
+ @c.set('another-key', 45)
38
+ @c.set('some-key-3', 90)
39
+
40
+ assert_equal [['some-key-3',90],['another-key',45]], @c.top(2)
41
+ end
42
+
43
+ should "break top(n) ties on key name" do
44
+ @c.set('some-key-1', 34)
45
+ @c.set('some-key-2', 34)
46
+
47
+ assert_equal [['some-key-1', 34]], @c.top(1)
48
+ end
49
+
50
+ end
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require 'timecop'
6
+
7
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
8
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
9
+ require 'counter'
10
+
11
+ class Test::Unit::TestCase
12
+ end
13
+
14
+ require 'counter/moving_count'
15
+
16
+ require 'active_record'
17
+ ActiveRecord::Base.establish_connection :adapter => 'mysql', :database => 'counter_test'
18
+ silence_stream(STDOUT) do
19
+ load(File.dirname(__FILE__) + "/schema.rb")
20
+ end
21
+
22
+ class PageView < MovingCount
23
+ end
24
+
25
+ class Click < MovingCount
26
+ set_sample_interval 5.seconds
27
+ set_history_to_keep 2.minutes
28
+ end
@@ -0,0 +1,115 @@
1
+ require 'test_helper'
2
+
3
+ class MovingCountTest < Test::Unit::TestCase
4
+
5
+ def setup
6
+ Timecop.return
7
+ [PageView, Click].each { |m| m.delete_all }
8
+ end
9
+
10
+ context "record" do
11
+
12
+ should "save counts to the database" do
13
+ PageView.record_counts do |c|
14
+ c.saw('http://www.nytimes.com')
15
+ c.saw('http://www.nytimes.com')
16
+ c.saw('http://www.nytimes.com/article.html')
17
+ end
18
+
19
+ assert_equal 2, PageView.find_by_category('http://www.nytimes.com').count
20
+ assert_equal 1, PageView.find_by_category('http://www.nytimes.com/article.html').count
21
+ end
22
+
23
+ context "sample interval" do
24
+ should "raise a RuntimeError to enforce sample interval" do
25
+ setup_existing_counts(59)
26
+ assert_raises(RuntimeError) { PageView.record_counts { |c| c.saw('http://www.nytimes.com') } }
27
+ end
28
+
29
+ should "default sample interval to 60 seconds" do
30
+ setup_existing_counts(60)
31
+ assert_nothing_raised { PageView.record_counts { |c| c.saw('http://www.nytimes.com') } }
32
+ end
33
+
34
+ should "respect custom sample interval" do
35
+ setup_existing_counts(5, Click)
36
+ assert_nothing_raised { Click.record_counts { |c| c.saw('http://www.nytimes.com') } }
37
+ end
38
+
39
+ should "allow samples to be backfilled" do
40
+ setup_existing_counts(60)
41
+ assert_nothing_raised { PageView.record_counts(Time.now - 3600) { |c| c.saw('http://www.nytimes.com') } }
42
+ end
43
+ end
44
+
45
+ context "sample_time" do
46
+ should "record provided timestamp as sample time" do
47
+ stamp = Time.now - 300
48
+ PageView.record_counts(stamp) do |c|
49
+ c.saw('http://www.nytimes.com')
50
+ c.saw('http://www.nytimes.com/article.html')
51
+ end
52
+
53
+ assert_equal 2, PageView.count(:conditions => {:sample_time => stamp})
54
+ end
55
+
56
+ should "use Time.now as the timestamp if none specified" do
57
+ Timecop.freeze
58
+
59
+ PageView.record_counts do |c|
60
+ c.saw('http://www.nytimes.com')
61
+ c.saw('http://www.nytimes.com/article.html')
62
+ end
63
+
64
+ assert_equal 2, PageView.count(:conditions => {:sample_time => Time.now})
65
+ end
66
+ end
67
+
68
+ context "purge" do
69
+ should "purge data older than history_to_keep" do
70
+ setup_existing_counts(2.hours)
71
+ PageView.record_counts { |c| c.saw('http://www.nytimes.com') }
72
+ assert_equal 1, PageView.count
73
+ end
74
+
75
+ should "set default history_to_keep at 1 hour" do
76
+ setup_existing_counts(59.minutes)
77
+ PageView.record_counts { |c| c.saw('http://www.nytimes.com') }
78
+ assert_equal 2, PageView.count
79
+ end
80
+
81
+ should "respect custom history_to_keep" do
82
+ setup_existing_counts(5.minutes, Click)
83
+ Click.record_counts { |c| c.saw('http://www.nytimes.com') }
84
+ assert_equal 1, Click.count
85
+ end
86
+ end
87
+
88
+
89
+ context "totals" do
90
+ should "returns totals across all samples in the history" do
91
+ setup_existing_counts(10.minutes)
92
+ setup_existing_counts(5.minutes)
93
+ PageView.record_counts { |c| c.saw('http://www.nytimes.com'); c.saw('http://www.nytimes.com/article.html') }
94
+
95
+ assert_equal [['http://www.nytimes.com',3],['http://www.nytimes.com/article.html',1]], PageView.totals
96
+ end
97
+
98
+ should "respect limit if provided" do
99
+ setup_existing_counts(10.minutes)
100
+ setup_existing_counts(5.minutes)
101
+ PageView.record_counts { |c| c.saw('http://www.nytimes.com'); c.saw('http://www.nytimes.com/article.html') }
102
+
103
+ assert_equal [['http://www.nytimes.com',3]], PageView.totals(1)
104
+ end
105
+ end
106
+
107
+ end
108
+
109
+ def setup_existing_counts distance, model=PageView
110
+ Timecop.freeze(Time.now - distance)
111
+ model.record_counts { |c| c.saw('http://www.nytimes.com') }
112
+ Timecop.freeze(Time.now + distance)
113
+ end
114
+
115
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: counter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Ben Koski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-07-29 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: count things, either as a one-off or aggregated over time
26
+ email: bkoski@nytimes.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION
41
+ - counter.gemspec
42
+ - lib/counter.rb
43
+ - lib/counter/counter.rb
44
+ - lib/counter/moving_count.rb
45
+ - test/schema.rb
46
+ - test/test_counter.rb
47
+ - test/test_helper.rb
48
+ - test/test_moving_count.rb
49
+ has_rdoc: true
50
+ homepage: http://github.com/nytimes/counter
51
+ licenses: []
52
+
53
+ post_install_message:
54
+ rdoc_options:
55
+ - --charset=UTF-8
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.3.5
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: count things, either as a one-off or aggregated over time
77
+ test_files:
78
+ - test/schema.rb
79
+ - test/test_counter.rb
80
+ - test/test_helper.rb
81
+ - test/test_moving_count.rb