juan_code18
juan_code18

Reputation: 253

Solving test for ruby hash `where` behavior

I am trying to write a function that will solve a series of tests. It is an array of hashes. I got started and my solution solves two of the tests but i feel as though my solution might not be the best.

I tried to access the rows using the key provided but ran into issues. So my next move was to loop through the fields in each row and then check it that matches the value passed in to the where method.

class Array
  def where(options)
    result = []

    keys = options.keys.to_s
    values = options.values
    puts options.keys
    self.each do |row|
      row.each do |k, v|
        if values.include?(v)
          result << row
        end
      end
    end
    result
  end
end

The tests are below:

require 'minitest/autorun'

class WhereTest < Minitest::Test
  def setup
    @boris   = {:name => 'Boris The Blade', :quote => "Heavy is good. Heavy is reliable. If it doesn't work you can always hit them.", :title => 'Snatch', :rank => 4}
    @charles = {:name => 'Charles De Mar', :quote => 'Go that way, really fast. If something gets in your way, turn.', :title => 'Better Off Dead', :rank => 3}
    @wolf    = {:name => 'The Wolf', :quote => 'I think fast, I talk fast and I need you guys to act fast if you wanna get out of this', :title => 'Pulp Fiction', :rank => 4}
    @glen    = {:name => 'Glengarry Glen Ross', :quote => "Put. That coffee. Down. Coffee is for closers only.",  :title => "Blake", :rank => 5}

    @fixtures = [@boris, @charles, @wolf, @glen]
  end

  def test_where_with_exact_match
    assert_equal [@wolf], @fixtures.where(:name => "The Wolf")
  end

  def test_where_with_partial_match
    assert_equal [@charles, @glen], @fixtures.where(:title => /^B.*/)
  end
  #
  def test_where_with_mutliple_exact_results
    assert_equal [@boris, @wolf], @fixtures.where(:rank => 4)
  end
  #
  def test_with_with_multiple_criteria
    assert_equal [@wolf], @fixtures.where(:rank => 4, :quote => /get/)
  end

  def test_with_chain_calls
    assert_equal [@charles], @fixtures.where(:quote => /if/i).where(:rank => 3)
  end
end

Upvotes: 0

Views: 56

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110725

You may wish to write your method Array#where as follows:

Code

class Array
  def where(select_hash)
    select do |h|
      select_hash.all? { |k,v| v === h[k] }
    end
  end
end

Examples

boris   = {:name => 'Boris The Blade', :title => 'Snatch', :rank => 4,
           :quote => "Heavy is good. If it doesn't work you can always hit them."}
charles = {:name => 'Charles De Mar', :title => 'Better Off Dead', :rank => 3,
            :quote => 'Go that way. If something gets in your way, turn.' }
wolf    = {:name => 'The Wolf', :title => 'Pulp Fiction', :rank => 4,
           :quote => 'I need you guys to act fast if you wanna get out of this'}
glen    = {:name => 'Glengarry Glen Ross', :title => "Blake", :rank => 5,
           :quote => "Put. That coffee. Down. Coffee is for closers only."}
wanda   = {:arr => [1, 2, 3] }
fixtures = [boris, charles, wolf, glen, wanda]

fixtures.where(:name => "The Wolf")               # [wolf]
  #=> [{:name=>"The Wolf", :title=>"Pulp Fiction", :rank=>4,
  #     :quote=>"I need you guys to act fast if you wanna get out of this"}] 
fixtures.where(:title => /^B.*/)                  # [charles, glen]
  #=> [{:name=>"Charles De Mar", :title=>"Better Off Dead", :rank=>3,
  #     :quote=>"Go that way. If something gets in your way, turn."},
  #    {:name=>"Glengarry Glen Ross", :title=>"Blake", :rank=>5,
  #     :quote=>"Put. That coffee. Down. Coffee is for closers only."}] 
fixtures.where(:rank => 4)                        # [boris, wolf]
  #=> [{:name=>"Boris The Blade", :title=>"Snatch", :rank=>4,
  #     :quote=>"Heavy is good. If it doesn't work you can always hit them."},
  #    {:name=>"The Wolf", :title=>"Pulp Fiction", :rank=>4,
  #     :quote=>"I need you guys to act fast if you wanna get out of this"}] 
fixtures.where(:rank => 4, :quote => /get/)       # [wolf]
  #=> [{:name=>"The Wolf", :title=>"Pulp Fiction", :rank=>4,
  #     :quote=>"I need you guys to act fast if you wanna get out of this"}] 
fixtures.where(:quote => /if/i).where(:rank => 3) # [charles]
  #=> [{:name=>"Charles De Mar", :title=>"Better Off Dead", :rank=>3,
  #     :quote=>"Go that way. If something gets in your way, turn."}]
fixtures.where(:arr => Array)                     # [wanda]
  #=> [{:arr=>[1, 2, 3]}]

@Amadan makes a good point that polluting the class Array is not a good idea. Using refinements is a possibility, but I still find that distasteful, as the method requires a receiver that is a particular type of array, one whose elements are hashes. Array methods, by contrast, operate on arbitrary arrays. Instead, I would suggest making the array a second argument (i.e., where(arr, select_hash)).

Upvotes: 2

Amadan
Amadan

Reputation: 198436

First, I don't like direct modification of base classes, not now that we have refinements. You can just use the method definition if you don't want the refinement, it will work the same.

Second, you are replicating a lot of work that Ruby already knows how to do. In particular, keeping or removing elements from an array is the job of #select, #select!, #keep_if, #delete_if and the like; while it is certainly possible to not use them, you will get a performance hit (since those functions are written in C, and will be faster than anything you can do in Ruby itself), and a readability hit (as you'll need more lines for the same job if you're not using available tools but crafting your own). I start with the full array, then whittle it down to only the elements that match all of the conditions. Also, note that I use dup to start with here - otherwise, keep_if would be removing elements from the fixture itself!

Third, what you thought you could get with #include?, you get from the subsumption operator ===. It is always somewhat hard to explain what === actually does, but in general, it looks like condition === value and you'll get true (or truthy) if the value is described by the condition. It works for containers having an element or not, it works with regexps matching a string or not, it works with predicates describing a value, and also with simple values being equal to other simple values. See === vs. == in Ruby for better explanations than mine.

module ArrayWithWhere
  refine Array do

    def where(conditions)
      dup.tap do |result|
        conditions.each do |field, condition|
          result.keep_if { |element| condition === element[field] }
        end
      end
    end

  end
end

class WhereTest < Minitest::Test
  using ArrayWithWhere

  # ...
end

Upvotes: 3

Related Questions