cohort_scope 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -13,7 +13,7 @@ begin
13
13
  gem.add_dependency "activesupport", ">=3.0.0.beta4"
14
14
  gem.add_dependency "activerecord", ">=3.0.0.beta4"
15
15
  gem.add_development_dependency "shoulda", ">= 2.10.3"
16
- # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ gem.add_development_dependency 'sqlite3-ruby'
17
17
  end
18
18
  Jeweler::GemcutterTasks.new
19
19
  rescue LoadError
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.7
1
+ 0.1.0
data/cohort_scope.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{cohort_scope}
8
- s.version = "0.0.7"
8
+ s.version = "0.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Seamus Abshere", "Andy Rossmeissl", "Derek Kastner"]
12
- s.date = %q{2010-08-20}
12
+ s.date = %q{2010-10-15}
13
13
  s.description = %q{Provides big_cohort, which widens by finding the constraint that eliminates the most records and removing it. Also provides strict_cohort, which widens by eliminating constraints in order.}
14
14
  s.email = %q{seamus@abshere.net}
15
15
  s.extra_rdoc_files = [
@@ -25,7 +25,11 @@ Gem::Specification.new do |s|
25
25
  "VERSION",
26
26
  "cohort_scope.gemspec",
27
27
  "lib/cohort_scope.rb",
28
+ "lib/cohort_scope/big_cohort.rb",
29
+ "lib/cohort_scope/cohort.rb",
30
+ "lib/cohort_scope/strict_cohort.rb",
28
31
  "test/helper.rb",
32
+ "test/test_cohort.rb",
29
33
  "test/test_cohort_scope.rb"
30
34
  ]
31
35
  s.homepage = %q{http://github.com/seamusabshere/cohort_scope}
@@ -35,6 +39,7 @@ Gem::Specification.new do |s|
35
39
  s.summary = %q{Provides cohorts (in the form of ActiveRecord scopes) that dynamically widen until they contain a certain number of records.}
36
40
  s.test_files = [
37
41
  "test/helper.rb",
42
+ "test/test_cohort.rb",
38
43
  "test/test_cohort_scope.rb"
39
44
  ]
40
45
 
@@ -46,15 +51,18 @@ Gem::Specification.new do |s|
46
51
  s.add_runtime_dependency(%q<activesupport>, [">= 3.0.0.beta4"])
47
52
  s.add_runtime_dependency(%q<activerecord>, [">= 3.0.0.beta4"])
48
53
  s.add_development_dependency(%q<shoulda>, [">= 2.10.3"])
54
+ s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
49
55
  else
50
56
  s.add_dependency(%q<activesupport>, [">= 3.0.0.beta4"])
51
57
  s.add_dependency(%q<activerecord>, [">= 3.0.0.beta4"])
52
58
  s.add_dependency(%q<shoulda>, [">= 2.10.3"])
59
+ s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
53
60
  end
54
61
  else
55
62
  s.add_dependency(%q<activesupport>, [">= 3.0.0.beta4"])
56
63
  s.add_dependency(%q<activerecord>, [">= 3.0.0.beta4"])
57
64
  s.add_dependency(%q<shoulda>, [">= 2.10.3"])
65
+ s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
58
66
  end
59
67
  end
60
68
 
data/lib/cohort_scope.rb CHANGED
@@ -1,28 +1,12 @@
1
1
  require 'active_record'
2
2
  require 'active_support'
3
3
  require 'active_support/version'
4
- %w{
5
- active_support/core_ext/module/delegation
6
- }.each do |active_support_3_requirement|
7
- require active_support_3_requirement
8
- end if ActiveSupport::VERSION::MAJOR == 3
9
4
 
10
- module ActiveRecord
11
- class Relation
12
- def inspect_count_only!
13
- @inspect_count_only = true
14
- end
15
- def inspect_count_only?
16
- @inspect_count_only == true
17
- end
18
- def as_json(*)
19
- inspect_count_only? ? { :members => count } : super
20
- end
21
- def inspect
22
- inspect_count_only? ? "<Massive ActiveRecord scope with #{count} members>" : super
23
- end
24
- end
25
- end
5
+ require 'active_support/core_ext/module/delegation' if ActiveSupport::VERSION::MAJOR == 3
6
+
7
+ require 'cohort_scope/cohort'
8
+ require 'cohort_scope/big_cohort'
9
+ require 'cohort_scope/strict_cohort'
26
10
 
27
11
  module CohortScope
28
12
  def self.extended(klass)
@@ -31,9 +15,9 @@ module CohortScope
31
15
 
32
16
  # Find the biggest scope possible by removing constraints <b>in any order</b>.
33
17
  # Returns an empty scope if it can't meet the minimum scope size.
34
- def big_cohort(constraints = {}, custom_minimum_cohort_size = nil)
18
+ def big_cohort(constraints = {}, custom_minimum_cohort_size = self.minimum_cohort_size)
35
19
  raise ArgumentError, "You can't give a big_cohort an OrderedHash; do you want strict_cohort?" if constraints.is_a?(ActiveSupport::OrderedHash)
36
- _cohort_scope constraints, custom_minimum_cohort_size
20
+ BigCohort.create self, constraints, custom_minimum_cohort_size
37
21
  end
38
22
 
39
23
  # Find the first acceptable scope by removing constraints <b>in strict order</b>, starting with the last constraint.
@@ -52,107 +36,8 @@ module CohortScope
52
36
  #
53
37
  # If the original constraints don't meet the minimum scope size, then the only constraint that can be removed is birthdate.
54
38
  # In other words, this would never return a scope that was constrained on birthdate but not on favorite_color.
55
- def strict_cohort(constraints, custom_minimum_cohort_size = nil)
39
+ def strict_cohort(constraints, custom_minimum_cohort_size = self.minimum_cohort_size)
56
40
  raise ArgumentError, "You need to give strict_cohort an OrderedHash" unless constraints.is_a?(ActiveSupport::OrderedHash)
57
- _cohort_scope constraints, custom_minimum_cohort_size
58
- end
59
-
60
- protected
61
-
62
- # Recursively look for a scope that meets the constraints and is at least <tt>minimum_cohort_size</tt>.
63
- def _cohort_scope(constraints, custom_minimum_cohort_size)
64
- raise RuntimeError, "You need to set #{name}.minimum_cohort_size = X" unless minimum_cohort_size.present?
65
-
66
- if constraints.values.none? # failing base case
67
- empty_cohort = scoped.where '1 = 2'
68
- empty_cohort.inspect_count_only!
69
- return empty_cohort
70
- end
71
-
72
- this_hash = _cohort_constraints constraints
73
- this_count = scoped.where(this_hash).count
74
-
75
- if this_count >= (custom_minimum_cohort_size || minimum_cohort_size) # successful base case
76
- cohort = scoped.where this_hash
77
- else
78
- cohort = _cohort_scope _cohort_reduce_constraints(constraints), custom_minimum_cohort_size
79
- end
80
- cohort.inspect_count_only!
81
- cohort
82
- end
83
-
84
- # Sanitize constraints by
85
- # * removing nil constraints (so constraints like "X IS NULL" are impossible, sorry)
86
- # * converting ActiveRecord::Base objects into integer foreign key constraints
87
- def _cohort_constraints(constraints)
88
- new_hash = constraints.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash.new : Hash.new
89
- conditions = constraints.inject(new_hash) do |memo, tuple|
90
- k, v = tuple
91
- if v.kind_of?(ActiveRecord::Base)
92
- condition = { _cohort_association_primary_key(k) => v.to_param }
93
- elsif !v.nil?
94
- condition = { k => v }
95
- end
96
- memo.merge! condition if condition.is_a? Hash
97
- memo
98
- end
99
- conditions
100
- end
101
-
102
- # Convert constraints that are provided as ActiveRecord::Base objects into their corresponding integer primary keys.
103
- #
104
- # Only works for <tt>belongs_to</tt> relationships.
105
- #
106
- # For example, :car => <#Car> might get translated into :car_id => 44.
107
- def _cohort_association_primary_key(name)
108
- @_cohort_association_primary_keys ||= {}
109
- return @_cohort_association_primary_keys[name] if @_cohort_association_primary_keys.has_key? name
110
- a = reflect_on_association name
111
- raise "can't use cohort scope on :through associations (#{self.name} #{name})" if a.options.has_key? :through
112
- if !a.primary_key_name.blank?
113
- @_cohort_association_primary_keys[name] = a.primary_key_name
114
- else
115
- raise "we need some other way to find primary key"
116
- end
117
- end
118
-
119
- # Choose how to reduce constraints based on whether we're looking for a big cohort or a strict cohort.
120
- def _cohort_reduce_constraints(constraints)
121
- case constraints
122
- when ActiveSupport::OrderedHash
123
- _cohort_reduce_constraints_in_order constraints
124
- when Hash
125
- _cohort_reduce_constraints_seeking_maximum_count constraints
126
- else
127
- raise "what did you pass me? #{constraints}"
128
- end
129
- end
130
-
131
- # (Used by <tt>big_cohort</tt>)
132
- #
133
- # Reduce constraints by removing them one by one and counting the results.
134
- #
135
- # The constraint whose removal leads to the highest record count is removed from the overall constraint set.
136
- def _cohort_reduce_constraints_seeking_maximum_count(constraints)
137
- highest_count_after_removal = nil
138
- losing_key = nil
139
- constraints.keys.each do |key|
140
- test_constraints = constraints.except(key)
141
- count_after_removal = scoped.where(_cohort_constraints(test_constraints)).count
142
- if highest_count_after_removal.nil? or count_after_removal > highest_count_after_removal
143
- highest_count_after_removal = count_after_removal
144
- losing_key = key
145
- end
146
- end
147
- constraints.except losing_key
148
- end
149
-
150
- # (Used by <tt>strict_cohort</tt>)
151
- #
152
- # Reduce constraints by removing the least important one.
153
- def _cohort_reduce_constraints_in_order(constraints)
154
- reduced_constraints = constraints.dup
155
- reduced_constraints.delete constraints.keys.last
156
- reduced_constraints
41
+ StrictCohort.create self, constraints, custom_minimum_cohort_size
157
42
  end
158
43
  end
@@ -0,0 +1,21 @@
1
+ module CohortScope
2
+ class BigCohort < Cohort
3
+
4
+ # Reduce constraints by removing them one by one and counting the results.
5
+ #
6
+ # The constraint whose removal leads to the highest record count is removed from the overall constraint set.
7
+ def self.reduce_constraints(model, constraints)
8
+ highest_count_after_removal = nil
9
+ losing_key = nil
10
+ constraints.keys.each do |key|
11
+ test_constraints = constraints.except(key)
12
+ count_after_removal = model.scoped.where(sanitize_constraints(model, test_constraints)).count
13
+ if highest_count_after_removal.nil? or count_after_removal > highest_count_after_removal
14
+ highest_count_after_removal = count_after_removal
15
+ losing_key = key
16
+ end
17
+ end
18
+ constraints.except losing_key
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ require 'delegate'
2
+
3
+ module CohortScope
4
+ class Cohort < Delegator
5
+
6
+ class << self
7
+ # Recursively look for a scope that meets the constraints and is at least <tt>minimum_cohort_size</tt>.
8
+ def create(model, constraints, custom_minimum_cohort_size)
9
+ raise RuntimeError, "You need to set #{name}.minimum_cohort_size = X" unless model.minimum_cohort_size.present?
10
+
11
+ if constraints.values.none? # failing base case
12
+ empty_cohort = model.scoped.where '1 = 2'
13
+ return new(empty_cohort)
14
+ end
15
+
16
+ constraint_hash = sanitize_constraints model, constraints
17
+ constrained_scope = model.scoped.where(constraint_hash)
18
+
19
+ if constrained_scope.count >= custom_minimum_cohort_size
20
+ new constrained_scope
21
+ else
22
+ reduced_constraints = reduce_constraints(model, constraints)
23
+ create(model, reduced_constraints, custom_minimum_cohort_size)
24
+ end
25
+ end
26
+
27
+ # Sanitize constraints by
28
+ # * removing nil constraints (so constraints like "X IS NULL" are impossible, sorry)
29
+ # * converting ActiveRecord::Base objects into integer foreign key constraints
30
+ def sanitize_constraints(model, constraints)
31
+ new_hash = constraints.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash.new : Hash.new
32
+ conditions = constraints.inject(new_hash) do |memo, tuple|
33
+ k, v = tuple
34
+ if v.kind_of?(ActiveRecord::Base)
35
+ primary_key = association_primary_key(model, k)
36
+ param = v.respond_to?(primary_key) ? v.send(primary_key) : v.to_param
37
+ condition = { primary_key => param }
38
+ elsif !v.nil?
39
+ condition = { k => v }
40
+ end
41
+ memo.merge! condition if condition.is_a? Hash
42
+ memo
43
+ end
44
+ conditions
45
+ end
46
+
47
+ # Convert constraints that are provided as ActiveRecord::Base objects into their corresponding integer primary keys.
48
+ #
49
+ # Only works for <tt>belongs_to</tt> relationships.
50
+ #
51
+ # For example, :car => <#Car> might get translated into :car_id => 44.
52
+ def association_primary_key(model, name)
53
+ @_cohort_association_primary_keys ||= {}
54
+ return @_cohort_association_primary_keys[name] if @_cohort_association_primary_keys.has_key? name
55
+ a = model.reflect_on_association name
56
+ raise "there is no association #{name.inspect} on #{model}" if a.nil?
57
+ raise "can't use cohort scope on :through associations (#{self.name} #{name})" if a.options.has_key? :through
58
+ if !a.primary_key_name.blank?
59
+ @_cohort_association_primary_keys[name] = a.primary_key_name
60
+ else
61
+ raise "we need some other way to find primary key"
62
+ end
63
+ end
64
+ end
65
+
66
+ def initialize(obj)
67
+ @_ch_obj = obj
68
+ end
69
+ def __getobj__
70
+ @_ch_obj
71
+ end
72
+
73
+ def as_json(*)
74
+ { :members => count }
75
+ end
76
+
77
+ def inspect
78
+ "<Massive ActiveRecord scope with #{count} members>"
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,13 @@
1
+ module CohortScope
2
+ class StrictCohort < Cohort
3
+
4
+ # (Used by <tt>strict_cohort</tt>)
5
+ #
6
+ # Reduce constraints by removing the least important one.
7
+ def self.reduce_constraints(model, constraints)
8
+ reduced_constraints = constraints.dup
9
+ reduced_constraints.delete constraints.keys.last
10
+ reduced_constraints
11
+ end
12
+ end
13
+ end
data/test/helper.rb CHANGED
@@ -18,10 +18,8 @@ ActiveSupport::Notifications.subscribe do |*args|
18
18
  end
19
19
 
20
20
  ActiveRecord::Base.establish_connection(
21
- 'adapter' => 'mysql',
22
- 'database' => 'cohort_scope_test',
23
- 'username' => 'root',
24
- 'password' => 'password'
21
+ 'adapter' => 'sqlite3',
22
+ 'database' => ':memory:'
25
23
  )
26
24
 
27
25
  ActiveRecord::Schema.define(:version => 20090819143429) do
@@ -30,6 +28,19 @@ ActiveRecord::Schema.define(:version => 20090819143429) do
30
28
  t.string 'favorite_color'
31
29
  t.integer 'teeth'
32
30
  end
31
+ create_table 'houses', :force => true do |t|
32
+ t.string 'period'
33
+ t.string 'address'
34
+ t.integer 'storeys'
35
+ end
36
+ create_table 'styles', :force => true do |t|
37
+ t.string 'period'
38
+ t.string 'name'
39
+ end
40
+ create_table 'residents', :force => true do |t|
41
+ t.integer 'house_id'
42
+ t.string 'name'
43
+ end
33
44
  end
34
45
 
35
46
  class Citizen < ActiveRecord::Base
@@ -54,3 +65,28 @@ end
54
65
  ].each do |birthdate, favorite_color, teeth|
55
66
  Citizen.create! :birthdate => birthdate, :favorite_color => favorite_color, :teeth => teeth
56
67
  end
68
+
69
+ class Style < ActiveRecord::Base
70
+ extend CohortScope
71
+ self.minimum_cohort_size = 3
72
+ has_many :houses
73
+ end
74
+ class House < ActiveRecord::Base
75
+ belongs_to :style, :foreign_key => 'period', :primary_key => 'period'
76
+ has_one :resident
77
+ end
78
+ class Resident < ActiveRecord::Base
79
+ has_one :house
80
+ end
81
+
82
+ Style.create! :period => 'arts and crafts', :name => 'classical revival'
83
+ Style.create! :period => 'arts and crafts', :name => 'gothic'
84
+ Style.create! :period => 'arts and crafts', :name => 'art deco'
85
+ Style.create! :period => 'victorian', :name => 'stick-eastlake'
86
+ Style.create! :period => 'victorian', :name => 'queen anne'
87
+ h1 = House.create! :period => 'arts and crafts', :address => '123 Maple', :storeys => 1
88
+ h2 = House.create! :period => 'arts and crafts', :address => '223 Walnut', :storeys => 2
89
+ h3 = House.create! :period => 'victorian', :address => '323 Pine', :storeys => 2
90
+ Resident.create! :house => h1, :name => 'Bob'
91
+ Resident.create! :house => h2, :name => 'Rob'
92
+ Resident.create! :house => h3, :name => 'Gob'
@@ -0,0 +1,44 @@
1
+ require 'helper'
2
+
3
+ class TestCohortScope < Test::Unit::TestCase
4
+ def setup
5
+ Citizen.minimum_cohort_size = 3
6
+ @date_range = (Date.parse('1980-01-01')..Date.parse('1990-01-01'))
7
+ end
8
+
9
+ def style
10
+ @style ||= Style.find_by_period 'arts and crafts'
11
+ end
12
+
13
+ context '.sanitize_constraints' do
14
+ should 'remove nil constraints' do
15
+ constraints = CohortScope::Cohort.sanitize_constraints Style, :eh => :tu, :bru => :te, :caesar => nil
16
+ assert_does_not_contain constraints.keys, :caesar
17
+ end
18
+ should 'keep normal constraints' do
19
+ constraints = CohortScope::Cohort.sanitize_constraints Style, :eh => :tu, :bru => :te, :caesar => nil
20
+ assert_equal :tu, constraints[:eh]
21
+ end
22
+ should 'include constraints that are models' do
23
+ gob = Resident.find_by_name 'Gob'
24
+ constraints = CohortScope::Cohort.sanitize_constraints House, :resident => gob
25
+ assert_equal gob.house_id, constraints[:house_id]
26
+ end
27
+ should 'include constraints that are models not related by primary key' do
28
+ constraints = CohortScope::Cohort.sanitize_constraints House, :style => style
29
+ assert_equal 'arts and crafts', constraints['period']
30
+ end
31
+ end
32
+
33
+ context '.association_primary_key' do
34
+ should 'include constraints that are models related by a primary key' do
35
+ gob = Resident.find_by_name('Gob')
36
+ key = CohortScope::Cohort.association_primary_key Resident, :house
37
+ assert_equal 'resident_id', key
38
+ end
39
+ should 'include constraints that are models related by a non-primary key' do
40
+ key = CohortScope::Cohort.association_primary_key House, :style
41
+ assert_equal 'period', key
42
+ end
43
+ end
44
+ end
@@ -26,6 +26,12 @@ class TestCohortScope < Test::Unit::TestCase
26
26
  assert_equal "<Massive ActiveRecord scope with 9 members>", cohort.inspect
27
27
  end
28
28
 
29
+ should "retain the scope's original behavior" do
30
+ cohort = Citizen.big_cohort :birthdate => @date_range, :favorite_color => 'heliotrope'
31
+ assert_kind_of Citizen, cohort.last
32
+ assert_kind_of Citizen, cohort.where(:teeth => 31).first
33
+ end
34
+
29
35
  should "raise if no minimum_cohort_size is specified" do
30
36
  Citizen.minimum_cohort_size = nil
31
37
  assert_raises(RuntimeError) {
@@ -59,11 +65,6 @@ class TestCohortScope < Test::Unit::TestCase
59
65
  Citizen.big_cohort ActiveSupport::OrderedHash.new
60
66
  }
61
67
  end
62
-
63
- should "result in a relation that has inspect_count_only set" do
64
- cohort = Citizen.big_cohort :favorite_color => 'heliotrope'
65
- assert cohort.inspect_count_only?
66
- end
67
68
  end
68
69
 
69
70
  context "strict_cohort" do
@@ -104,13 +105,5 @@ class TestCohortScope < Test::Unit::TestCase
104
105
  cohort = Citizen.strict_cohort birthdate_matters_most
105
106
  assert_equal 9, cohort.count
106
107
  end
107
-
108
- should "result in a relation that has inspect_count_only set" do
109
- ordered_attributes = ActiveSupport::OrderedHash.new
110
- ordered_attributes[:favorite_color] = 'heliotrope'
111
-
112
- cohort = Citizen.strict_cohort ordered_attributes
113
- assert cohort.inspect_count_only?
114
- end
115
108
  end
116
109
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cohort_scope
3
3
  version: !ruby/object:Gem::Version
4
- hash: 17
4
+ hash: 27
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 7
10
- version: 0.0.7
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Seamus Abshere
@@ -17,7 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2010-08-20 00:00:00 -04:00
20
+ date: 2010-10-15 00:00:00 -04:00
21
21
  default_executable:
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency
@@ -70,6 +70,20 @@ dependencies:
70
70
  version: 2.10.3
71
71
  type: :development
72
72
  version_requirements: *id003
73
+ - !ruby/object:Gem::Dependency
74
+ name: sqlite3-ruby
75
+ prerelease: false
76
+ requirement: &id004 !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ hash: 3
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ type: :development
86
+ version_requirements: *id004
73
87
  description: Provides big_cohort, which widens by finding the constraint that eliminates the most records and removing it. Also provides strict_cohort, which widens by eliminating constraints in order.
74
88
  email: seamus@abshere.net
75
89
  executables: []
@@ -88,7 +102,11 @@ files:
88
102
  - VERSION
89
103
  - cohort_scope.gemspec
90
104
  - lib/cohort_scope.rb
105
+ - lib/cohort_scope/big_cohort.rb
106
+ - lib/cohort_scope/cohort.rb
107
+ - lib/cohort_scope/strict_cohort.rb
91
108
  - test/helper.rb
109
+ - test/test_cohort.rb
92
110
  - test/test_cohort_scope.rb
93
111
  has_rdoc: true
94
112
  homepage: http://github.com/seamusabshere/cohort_scope
@@ -126,4 +144,5 @@ specification_version: 3
126
144
  summary: Provides cohorts (in the form of ActiveRecord scopes) that dynamically widen until they contain a certain number of records.
127
145
  test_files:
128
146
  - test/helper.rb
147
+ - test/test_cohort.rb
129
148
  - test/test_cohort_scope.rb