Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions lib/order.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
require_relative 'data_main'
require_relative 'parse'

module TagExpressions
module Order
ACCUMULATE = 100
SKIP = 0
def self.data
@@data = TagExpressions::Data::tags
end

def self.default_options
{:until_up_to => 0,
:until_less_than_or_equal_to => 0,
:skip => SKIP,
:accumulate => ACCUMULATE,
:starting_at => -1}
end

# use following API:
# :accumulate => accumulate this many items
# :skip => do not accumulate until this many matches are found
# :until_less_than => accumulate until PAST this value
# :until_up_to => accumulate UP TO this value
# :must_accumulate => return false if cannot accumulate this many values
def self.update_return_with_options(return_list, ref, count, opt = options)

return_list_orig_length = return_list.length

if return_list.length < opt[:accumulate] and ref >= opt[:until_up_to]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on abstracting some of these more complicated checks into private helper methods? That usually helps as a reminder of what the check is actually doing, and avoid nested ifs.

e.g.:

private

def self.foo?(return_list, ref, opt) # use a better method name
  return_list.length < opt[:accumulate] && ref >= opt[:until_up_to]
end

def self.bar?(return_list, ref, opt)
  return_list[-1] != ref || opt[:include_duplicates]
end

then this block of code can just be:

if foo? && bar?
  count[0] += 1
  ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(feel free to do this with some of the other if statements in the code as well... it makes everything more readable especially when you go back later imo)

if (return_list[-1] != ref or opt[:include_duplicates])
count[0] += 1
if count[0] > opt[:skip]
return_list.push(ref) if ref != nil
if opt.has_key? :until_less_than_or_equal_to and opt[:until_less_than_or_equal_to] >= ref
return false
end
end
end
else
return false
end
if opt.has_key?(:must_accumulate) and (return_list.length - return_list_orig_length) < opt[:must_accumulate]
return false
end
true
end

def self.union(set0, set1, opt = {})
opt = default_options.merge(opt)

count = [0]
return_list = []
set0_prev_length = 0
i = [-1, -1]

set0_temp = set0; set1_temp = set1
set0_proc = set0; set1_proc = set1 #default initialization
set0_proc = lambda{ |opt| base(set0_temp, opt).reverse} if !set0.kind_of? Proc
set1_proc = lambda{ |opt| base(set1_temp, opt).reverse} if !set1.kind_of? Proc

# inner option is to allow union to convert starting_at value to the accumulate value
# when it is not the outer call
if opt.delete(:inner)
opt[:skip] = opt[:starting_at] * -1 - 1
end

while return_list.length < opt[:accumulate] and (i[1] > -ACCUMULATE or i[0] > -ACCUMULATE)
ref1 = set0_proc.call({:accumulate => 1, :starting_at => i[0], :inner => true})
ref2 = set1_proc.call({:accumulate => 1, :starting_at => i[1], :inner => true})
refs = [ref1, ref2]

if ref1[0] == nil
ref = ref2[0]
i[1] -= 1
elsif ref2[0] == nil
ref = ref1[0]
i[0] -= 1

elsif ref1[0] > ref2[0]
ref = ref1[0]
i[0] -= 1
else
ref = ref2[0]
i[1] -= 1
end

if ref == nil or update_return_with_options(return_list, ref, count, opt) == false
break
end
end
return return_list
end

# id is added to list if set1 val is not equal to reference val
def self.difference(set0, set1, opt = {})
return deduction(set0, set1, opt) do |set0, set1|
true if set0[0] != set1[0]
end
end

# id is added to list if set1 val is equal to reference val
def self.intersection(set0, set1, opt = {})
return deduction(set0, set1, opt) do |set0, set1|
true if set0[0] == set1[0]
end
end


# iterate reference list by one
# advance the set1 until it is less than or equal to the first set
# yield returns condition
def self.deduction(set0, set1, opt = {})
#subtract set1 from set0
opt = default_options.merge(opt)
count = [0]
return_list = []
set0_prev_length = 0
i = [opt[:starting_at] || -1, -1]

set0_temp = set0; set1_temp = set1
set0_proc = set0; set1_proc = set1
set0_proc = lambda{ |opt| base(set0_temp, opt).reverse} if !set0.kind_of? Proc
set1_proc = lambda{ |opt| base(set1_temp, opt).reverse} if !set1.kind_of? Proc
while return_list.length < (opt[:accumulate]) and i[0] > -ACCUMULATE
set0 = set0_proc.call({:accumulate => 1, :starting_at => i[0], :inner => true})
if set0[0]
set1 = set1_proc.call({:until_less_than_or_equal_to => set0[0], :inner => true})

if yield set0, set1
break if update_return_with_options(return_list, set0[0], count, opt) == false
end

i[0] -= 1

else break
end
end
return return_list
end

# iterate a base case (reference to a list from db) that is not associated with a set operator
def self.base(set0, opt = {})
opt = default_options.merge(opt)
count = [0]
return_list = []
i = [opt[:starting_at] || -1, -1]
while return_list.length < (opt[:accumulate]) and i[0] > -ACCUMULATE and set0[i[0]] != nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nice substitute for foo != nil in rails is foo.present?

edit: never mind, forgot we weren't in rails.

break if update_return_with_options(return_list, set0[i[0]], count, opt) == false
i[0] -= 1
end
return return_list
end
end
end



##### some example code ######

ruby = TagExpressions::Order.data["Ruby"]; # p ruby.reverse
adult = TagExpressions::Order.data["Adult"]; # p adult.reverse
# p (ruby + adult).sort.uniq.reverse
air = TagExpressions::Order.data["Air"]; # p air.reverse
aeroplane = TagExpressions::Order.data["Aeroplane"]; #p aeroplane.reverse
album = TagExpressions::Order.data["Album"];# p album.reverse
# p (ruby + adult).sort.uniq.reverse

room = TagExpressions::Order.data["Room"]; # p room.reverse
# p (aeroplane & air).uniq.sort.reverse
# puts
# ruby.reverse
# adult.reverse

# puts

air_plus_aeroplane = lambda{ |opt| TagExpressions::Order.union(air, aeroplane, opt).reverse }
air_and_aeroplane = lambda{ |opt| TagExpressions::Order.intersection(air, aeroplane, opt).reverse }
ruby_plus_adult = lambda{ |opt| TagExpressions::Order.union(ruby, air_and_aeroplane, opt).reverse }

ruby_and_adult = lambda{ |opt| TagExpressions::Order.intersection_base(ruby, air_and_aeroplane, opt).reverse }
ruby_plus_adult__and__air_plus_aeroplane = lambda{ |opt| TagExpressions::Order.intersection(ruby_plus_adult, air_plus_aeroplane, opt).reverse }

# p TagExpressions::Order.difference(ruby_plus_adult, air_plus_aeroplane )
# p TagExpressions::Order.union(ruby, adult)
# p ((ruby + adult) - (air + aeroplane)).sort.uniq.reverse
# p ((ruby + adult)).sort.uniq.reverse
# p TagExpressions::Order.intersection(ruby_plus_adult, air_plus_aeroplane)
# p ((ruby - adult) - (ruby & adult)).sort.reverse.uniq

# p "STARTING VERY VERY SLOW EVALUATION"
# p TagExpressions::Order.difference(ruby_plus_adult, ruby_plus_adult__and__air_plus_aeroplane)
# p ((ruby + (air & aeroplane)) - ((ruby + (air & aeroplane)) & (air + aeroplane))).sort{ |a,b| b <=> a }.uniq


113 changes: 113 additions & 0 deletions spec/order_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require_relative 'spec_helper'
require_relative 'models'

describe "General Order of Operation Tag Evaluation" do

lue = lambda{ TagExpressions.data["LUE"] }
programming = lambda{ TagExpressions.data["Programming"] }
java = lambda{ TagExpressions.data["Java"] }
heartbreaks = lambda{ TagExpressions.data["Heartbreaks"] }
current_events = lambda{ TagExpressions.data["current_events"] }
ruby = lambda{ TagExpressions.data["Ruby"] }
sports = lambda{ TagExpressions.data["Sports"] }


before(:each) do
TagExpressions::Data::reset_tags
TagExpressions::Data::DB::setup
end

after(:each) do
TagExpressions::Data::Topic.delete_all
TagExpressions::Data::Tag.delete_all
end

it 'should allow difference' do
topic1 = Topic::create_with_tags("LUE, Programming")
topic2 = Topic::create_with_tags("LUE, Programming")
topic3 = Topic::create_with_tags("LUE, Java")
Order.difference(lue.(), programming.()).should == (lue.() - programming.())
end

it 'should allow union' do
topic1 = Topic::create_with_tags("LUE")
topic2 = Topic::create_with_tags("Programming")
topic3 = Topic::create_with_tags("Java")

Order.union(Order.union(lue.(), programming.()).reverse, java.()).should == (lue.() + programming.() + java.()).sort.reverse

end

it 'should allow intersection' do
topic1 = Topic::create_with_tags("LUE, Programming, Java")
topic2 = Topic::create_with_tags("Programming, Java")
topic3 = Topic::create_with_tags("Java, Programming")

Order.intersection(Order.intersection(lue.(), programming.()), java.()).should == (lue.() & programming.() & java.())
end

it 'should handle all at once' do
topic1 = Topic::create_with_tags("LUE, Programming")
topic2 = Topic::create_with_tags("Java")
topic3 = Topic::create_with_tags("Programming, LUE, Heartbreaks")
topic4 = Topic::create_with_tags("Java, Programming, LUE, Heartbreaks")

lue_plus_java = Order.union(lue.(), java.()).reverse
lue_plus_java_minus_heartbreaks = Order.difference(lue_plus_java, heartbreaks.()).reverse

Order.intersection(programming.(), lue_plus_java_minus_heartbreaks).should == programming.() & lue.() + java.() - heartbreaks.()
end

it 'should handle complex cases' do
topic1 = Topic::create_with_tags("LUE, Programming")
topic2 = Topic::create_with_tags("Java, Programming")
topic3 = Topic::create_with_tags("Programming, LUE, Heartbreaks")
topic4 = Topic::create_with_tags("Java, Programming, LUE, Heartbreaks")
topic5 = Topic::create_with_tags("Current Events, Programming, Java, Ruby")
topic6 = Topic::create_with_tags("Ruby, Sports, Heartbreaks, Programming")
topic7 = Topic::create_with_tags("LUE")
topic8 = Topic::create_with_tags("Java, Current_Events, LUE, Heartbreaks")
topic9 = Topic::create_with_tags("LUE, Current Events, Programming, Java, Ruby, Heartbreaks")
topic10 = Topic::create_with_tags("LUE, Current Events, Programming, Java, Ruby")

lue__plus__java = Order.union(lue.(), java.()).reverse
lue__plus__java__minus__current_events = Order.difference(lue__plus__java, current_events.()).reverse
programming__plus__ruby = Order.union(programming.(), ruby.()).reverse
heartbreaks__minus__sports = Order.difference(heartbreaks.(), sports.()).reverse

Order.intersection(lue__plus__java__minus__current_events,
Order.intersection(programming__plus__ruby, heartbreaks__minus__sports).reverse).should ==
(lue.() + java.() - current_events.() & programming.() + ruby.() & heartbreaks.() - sports.()).sort.reverse
end
# it 'should be able to restrict accumulation' do
# topic1 = Topic::create_with_tags("Programming")
# topic2 = Topic::create_with_tags("Programming")
# topic3 = Topic::create_with_tags("Programming")
# topic4 = Topic::create_with_tags("Programming")
# topic5 = Topic::create_with_tags("Movies")
# topic6 = Topic::create_with_tags("Movies")
# topic7 = Topic::create_with_tags("Movies")

# options = {:accumulate => 5}
# result = Expressions::evaluate("Programming + Movies", options)
# result.should_not == [topic7, topic6, topic5, topic4, topic3, topic2, topic1]
# result.should == [topic7, topic6, topic5, topic4, topic3]

# end

# it 'should be able to skip results' do
# topic1 = Topic::create_with_tags("LUE")
# topic2 = Topic::create_with_tags("Programming")
# topic3 = Topic::create_with_tags("Java")
# topic4 = Topic::create_with_tags("Current_Events")
# topic5 = Topic::create_with_tags("Ruby")
# topic6 = Topic::create_with_tags("Movies")
# topic7 = Topic::create_with_tags("Pets")

# options = {:skip => 2}
# result = Expressions::evaluate("LUE + Programming + Java + Current_Events + Ruby + Movies + Pets", options)
# result.should_not == [topic7, topic6, topic5, topic4, topic3, topic2, topic1]
# result.should == [topic5, topic4, topic3, topic2, topic1]

# end
end