require 'helper' c = ActiveRecord::Base.connection c.create_table 'flights', :force => true do |t| t.string 'origin' t.string 'dest' t.string 'airline' t.string 'plane' end class Flight < ActiveRecord::Base end FactoryGirl.define do factory :lax, :class => Flight do origin 'LAX' end factory :lax_sfo, :class => Flight do origin 'LAX' dest 'SFO' end factory :lax_sfo_co, :class => Flight do origin 'LAX' dest 'SFO' airline 'Continental' end factory :lax_sfo_a320, :class => Flight do origin 'LAX' dest 'SFO' plane 'A320' end factory :lax_sfo_aa_a320, :class => Flight do origin 'LAX' dest 'SFO' airline 'American' plane 'A320' end end describe CohortAnalysis do before do Flight.delete_all end describe 'ActiveRecordBaseClassMethods' do describe :cohort do it "defaults to :minimum_size => 1" do FactoryGirl.create(:lax) Flight.cohort({:origin => 'LAX'}).count.must_equal 1 Flight.cohort({:origin => 'LAX'}, :minimum_size => 2).count.must_equal 0 end it "doesn't discard characteristics if it doesn't need to" do FactoryGirl.create(:lax) FactoryGirl.create(:lax_sfo) Flight.cohort(:origin => 'LAX', :dest => 'SFO').count.must_equal 1 end it "discards characteristics until it can fulfil the minimum size" do FactoryGirl.create(:lax) FactoryGirl.create(:lax_sfo) drops_dest = Flight.cohort({:origin => 'LAX', :dest => 'SFO'}, :minimum_size => 2) drops_dest.count.must_equal 2 drops_dest.one? { |flight| flight.dest != 'SFO' }.must_equal true end it "defaults to :strategy => :big" do FactoryGirl.create(:lax) Flight.cohort({:origin => 'LAX', :dest => 'SFO'}, :strategy => :big).count.must_equal Flight.cohort(:origin => 'LAX', :dest => 'SFO').count Flight.cohort({:dest => 'SFO', :origin => 'LAX'}, :strategy => :big).count.must_equal Flight.cohort(:dest => 'SFO', :origin => 'LAX').count end it "offers :strategy => :strict" do FactoryGirl.create(:lax) if RUBY_VERSION >= '1.9' # native ordered hashes Flight.cohort({:origin => 'LAX', :dest => 'SFO'}, :strategy => :strict).count.must_equal 1 Flight.cohort({:dest => 'SFO', :origin => 'LAX'}, :strategy => :strict).count.must_equal 0 else # activesupport provides ActiveSupport::OrderedHash origin_important = ActiveSupport::OrderedHash.new origin_important[:origin] = 'LAX' origin_important[:dest] = 'SFO' dest_important = ActiveSupport::OrderedHash.new dest_important[:dest] = 'SFO' dest_important[:origin] = 'LAX' Flight.cohort(origin_important, :strategy => :strict).count.must_equal 1 Flight.cohort(dest_important, :strategy => :strict).count.must_equal 0 lambda { Flight.cohort({:origin => 'LAX', :dest => 'SFO'}, :strategy => :strict).count }.must_raise(ArgumentError, 'hash') end end it "lets you pick :priority of keys when using :strict strategy" do FactoryGirl.create(:lax) Flight.cohort({:origin => 'LAX', :dest => 'SFO'}, :strategy => :strict, :priority => [:origin, :dest]).count.must_equal 1 Flight.cohort({:origin => 'LAX', :dest => 'SFO'}, :strategy => :strict, :priority => [:dest, :origin]).count.must_equal 0 Flight.cohort({:dest => 'SFO', :origin => 'LAX'}, :strategy => :strict, :priority => [:origin, :dest]).count.must_equal 1 Flight.cohort({:dest => 'SFO', :origin => 'LAX'}, :strategy => :strict, :priority => [:dest, :origin]).count.must_equal 0 end it "lets you play with more than 1 or 2 characteristics" do ActiveRecord::Base.silence do # make some fixtures 1_000.times { FactoryGirl.create(:lax) } 100.times { FactoryGirl.create(:lax_sfo) } 10.times { FactoryGirl.create(:lax_sfo_co) } 3.times { FactoryGirl.create(:lax_sfo_a320) } 1.times { FactoryGirl.create(:lax_sfo_aa_a320) } end Flight.count.must_equal 1_114 # sanity check lax_sfo_aa_a320 = {:origin => 'LAX', :dest => 'SFO', :airline => 'American', :plane => 'A320'} # don't discard anything Flight.cohort(lax_sfo_aa_a320).count.must_equal 1 # discard airline Flight.cohort(lax_sfo_aa_a320, :minimum_size => 2).count.must_equal 4 # discard plane and airline Flight.cohort(lax_sfo_aa_a320, :minimum_size => 5).count.must_equal 114 # discard plane and airline and dest Flight.cohort(lax_sfo_aa_a320, :minimum_size => 115).count.must_equal 1_114 lax_sfo_a320 = {:origin => 'LAX', :dest => 'SFO', :plane => 'A320'} # don't discard anything Flight.cohort(lax_sfo_a320).count.must_equal 4 # discard plane Flight.cohort(lax_sfo_a320, :minimum_size => 5).count.must_equal 114 # discard plane and dest Flight.cohort(lax_sfo_a320, :minimum_size => 115).count.must_equal 1_114 # off the rails here a bit woah_lax_co_a320 = {:origin => 'LAX', :airline => 'Continental', :plane => 'A320'} # discard plane Flight.cohort(woah_lax_co_a320).count.must_equal 10 # discard plane and airline Flight.cohort(woah_lax_co_a320, :minimum_size => 11).count.must_equal 1_114 end it "lets you play with multiple characteristics in :strategy => :strict" do ActiveRecord::Base.silence do # make some fixtures 1_000.times { FactoryGirl.create(:lax) } 100.times { FactoryGirl.create(:lax_sfo) } 10.times { FactoryGirl.create(:lax_sfo_co) } 3.times { FactoryGirl.create(:lax_sfo_a320) } 1.times { FactoryGirl.create(:lax_sfo_aa_a320) } end lax_sfo_aa_a320 = {:origin => 'LAX', :dest => 'SFO', :airline => 'American', :plane => 'A320'} priority = [:origin, :dest, :airline, :plane] # discard nothing Flight.cohort(lax_sfo_aa_a320, :strategy => :strict, :priority => priority).count.must_equal 1 # (force) discard plane, then (force) discard airline Flight.cohort(lax_sfo_aa_a320, :strategy => :strict, :priority => priority, :minimum_size => 2).count.must_equal 114 # (force) discard plane, then (force) discard airline, then (force) discard dest Flight.cohort(lax_sfo_aa_a320, :strategy => :strict, :priority => priority, :minimum_size => 115).count.must_equal 1_114 priority = [:plane, :airline, :dest, :origin] # discard nothing Flight.cohort(lax_sfo_aa_a320, :strategy => :strict, :priority => priority).count.must_equal 1 # (force) discard origin, then (force) discard dest, then (force) discard airline Flight.cohort(lax_sfo_aa_a320, :strategy => :strict, :priority => priority, :minimum_size => 2).count.must_equal 4 # gives up! Flight.cohort(lax_sfo_aa_a320, :strategy => :strict, :priority => priority, :minimum_size => 5).count.must_equal 0 end end describe :cohort_constraint do it "can be used like other ARel constraints" do FactoryGirl.create(:lax) Flight.where(Flight.cohort_constraint(:origin => 'LAX')).count.must_equal 1 Flight.where(Flight.cohort_constraint({:origin => 'LAX'}, :minimum_size => 2)).count.must_equal 0 end it "can be combined with other ARel constraints" do FactoryGirl.create(:lax) FactoryGirl.create(:lax_sfo) origin_lax_constraint = Flight.cohort_constraint(:origin => 'LAX') dest_sfo_constraint = Flight.arel_table[:dest].eq('SFO') Flight.where(dest_sfo_constraint.and(origin_lax_constraint)).count.must_equal 1 Flight.where(dest_sfo_constraint.or(origin_lax_constraint)).count.must_equal 2 Flight.where(origin_lax_constraint.and(dest_sfo_constraint)).count.must_equal 1 Flight.where(origin_lax_constraint.or(dest_sfo_constraint)).count.must_equal 2 end # Caution! it "is NOT smart enough to enforce minimum size when composed" do FactoryGirl.create(:lax) FactoryGirl.create(:lax_sfo) origin_lax_constraint = Flight.cohort_constraint({:origin => 'LAX'}, :minimum_size => 2) dest_sfo_constraint = Flight.arel_table[:dest].eq('SFO') Flight.where(dest_sfo_constraint.and(origin_lax_constraint)).count.must_equal 1 # see how minimum_size is ignored? Flight.where(origin_lax_constraint.and(dest_sfo_constraint)).count.must_equal 1 # it's because the cohort constraint resolves itself before allowing the ARel visitor to continue end end end describe 'ActiveRecordRelationInstanceMethods' do describe :cohort do it "is the proper way to compose when other ARel constraints are present" do FactoryGirl.create(:lax) FactoryGirl.create(:lax_sfo) Flight.where(:dest => 'SFO').cohort(:origin => 'LAX').count.must_equal 1 Flight.where(:dest => 'SFO').cohort({:origin => 'LAX'}, :minimum_size => 2).count.must_equal 0 end end describe :cohort_constraint do it "can also be used (carefully) to compose with other ARel constraints" do FactoryGirl.create(:lax) FactoryGirl.create(:lax_sfo) dest_sfo_relation = Flight.where(:dest => 'SFO') origin_lax_constraint_from_dest_sfo_relation = dest_sfo_relation.cohort_constraint(:origin => 'LAX') Flight.where(origin_lax_constraint_from_dest_sfo_relation).count.must_equal 1 dest_sfo_relation = Flight.where(:dest => 'SFO') origin_lax_constraint_from_dest_sfo_relation = dest_sfo_relation.cohort_constraint({:origin => 'LAX'}, :minimum_size => 2) Flight.where(origin_lax_constraint_from_dest_sfo_relation).count.must_equal 0 end end end end