cohort_scope 0.0.7 → 0.1.0

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