I've seen numerous questions about this but only with one key, never for multiple keys.

I have the following array of hashes:

a = [{:name=>"Yes, Yes, Yes", :artist=>"Some Dude", :composer=> 'First Dude', :duration=>"3:21"},
 {:name=>"Chick on the Side", :artist=>"Another Dude", :duration=>"3:20"},
 {:name=>"Luv Is", :duration=>"3:13"},
 {:name=>"Yes, Yes, Yes", :artist=>"Some Dude", :composer=> 'First Dude', :duration=>"2"},
 {:name=>"Chick on the Side", :artist=>"Another Dude"}]

a.uniq won't work here because the duration is different or might not even exist. I have a unique key set up in the database that does not allow duplicate entries by the same name, artist and composer so I sometimes get errors when people have duplicate entries for these 3 keys.

Is there a way to run uniq that would check for those 3 keys? I tried a block like this:

 new_tracks.uniq do |a_track|
   a_track[:name]
   a_track[:artist]
   a_track[:composer]
 end

But that ignores anything where the key is not present (any entry without a composer does not meet the above criteria for example).

I could always use just the :name key but that would mean I'm getting rid of potentially valid tracks in compilations that have the same title but different artist or composer.

This is with Ruby 2.0.

有帮助吗?

解决方案

uniq accepts a block. If a block is given, it will use the return value of the block for comparison.

Your code was close to the solution, but in your code the return value was only a_track[:composer] which is the last evaluated statement.

You can join the attributes you want into a string and return that string.

new_tracks.uniq { |track| [track[:name], track[:artist], track[:composer]].join(":") }

A possible refactoring is

new_tracks.uniq { |track| track.attributes.slice('name', 'artist', 'composer').values.join(":") }

Or add a custom method in your model that performs the join, and call it

class Track < ActiveRecord::Base
  def digest
    attributes.slice('name', 'artist', 'composer').values.join(":")
  end
end

new_tracks.uniq(&:digest)

其他提示

Another way to do it is to use values_at. if you don't want to use slice and join

a.uniq {|hash| hash.values_at(:name, :composer, :artist)}

If I understand your question, it's just a matter of using the right combination of data inside the uniq block:

a = [
  {:name=>"Yes, Yes, Yes", :artist=>"Some Dude", :composer=> 'First Dude', :duration=>"3:21"},
  {:name=>"Yes, Yes, Yes", :artist=>"Some Dude", :composer=> 'First Dude', :duration=>"2"},
  {:name=>"Chick on the Side", :artist=>"Another Dude", :duration=>"3:20"},
  {:name=>"Chick on the Side", :artist=>"Another Dude"},
  {:name=>"Luv Is", :duration=>"3:13"},
]

a.uniq{ |a_track|
  [
    a_track[:name],
    a_track[:artist],
    a_track[:composer],
  ]
} 

Which would return:

[
  {:name=>"Yes, Yes, Yes", :artist=>"Some Dude", :composer=>"First Dude", :duration=>"3:21"},
  {:name=>"Chick on the Side", :artist=>"Another Dude", :duration=>"3:20"},
  {:name=>"Luv Is", :duration=>"3:13"}
]

uniq lets us create anything inside its block, and uses that for its comparison. I'm choosing to use an array, because Ruby knows how to compare arrays, but the value could be a MD5 checksum or a CRC check if that made sense:

a.uniq{ |a_track|
  OpenSSL::Digest::MD5.digest(a_track[:name] + (a_track[:artist] || '') + (a_track[:composer] || ''))
} 
# => [{:name=>"Yes, Yes, Yes", :artist=>"Some Dude", :composer=>"First Dude", :duration=>"3:21"}, {:name=>"Chick on the Side", :artist=>"Another Dude", :duration=>"3:20"}, {:name=>"Luv Is", :duration=>"3:13"}]

I have to use (a_track[:artist] || '') because we can't concatenate a nil to a String, so || '' returns an empty string instead.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top