Uniq of ruby array fails to work

此生再无相见时 提交于 2019-12-11 01:27:38

问题


I have a array of my object Country which has the attributes "code" and "name"

The array could have a country in it more than once so I want to distinct the array.

This is my countries class

class Country
  include Mongoid::Fields::Serializable
  attr_accessor :name, :code

  FILTERS = ["Afghanistan","Brunei","Iran", "Kuwait", "Libya", "Saudi Arabia", "Sudan", "Yemen", "Britain (UK)", "Antarctica", "Bonaire Sint Eustatius & Saba", "British Indian Ocean Territory", "Cocos (Keeling) Islands", "St Barthelemy", "St Martin (French part)", "Svalbard & Jan Mayen","Vatican City"]

  EXTRAS = {
    'eng' => 'England',
    'wal' => 'Wales',
    'sco' => 'Scotland',
    'nlr' => 'Northern Ireland'
    }

  def initialize(name, code)
    @name = name
    @code = code
  end

  def deserialize(object)
    return nil unless object
    Country.new(object['name'], object['code'])
  end

  def serialize(country)
    {:name => country.name, :code => country.code}
  end

  def self.all
    add_extras(filter(TZInfo::Country.all.map{|country| to_country country})).sort! {|c1, c2| c1.name <=> c2.name}
  end

  def self.get(code)
    begin
      to_country TZInfo::Country.get(code)
    rescue TZInfo::InvalidCountryCode => e
      'InvalidCountryCode' unless EXTRAS.has_key? code
      Country.new EXTRAS[code], code
    end
  end

  def self.get_by_name(name)
    all.select {|country| country.name.downcase == name.downcase}.first
  end

  def self.filter(countries)
    countries.reject {|country| FILTERS.include?(country.name)}
  end

  def self.add_extras(countries)
    countries + EXTRAS.map{|k,v| Country.new v, k}
  end

  private
  def self.to_country(country)
    Country.new country.name, country.code
  end
end

and my request for the array which is called from another class

  def countries_ive_drunk
    (had_drinks.map {|drink| drink.beer.country }).uniq
  end

If I throw the array I can see the structure is:

[
#<Country:0x5e3b4c8 @name="Belarus", @code="BY">, 
#<Country:0x5e396e0 @name="Britain (UK)", @code="GB">, 
#<Country:0x5e3f350 @name="Czech Republic", @code="CZ">, 
#<Country:0x5e3d730 @name="Germany", @code="DE">, 
#<Country:0x5e43778 @name="United States", @code="US">, 
#<Country:0x5e42398 @name="England", @code="eng">, 
#<Country:0x5e40f70 @name="Aaland Islands", @code="AX">, 
#<Country:0x5e47978 @name="England", @code="eng">, 
#<Country:0x5e46358 @name="Portugal", @code="PT">, 
#<Country:0x5e44d38 @name="Georgia", @code="GE">, 
#<Country:0x5e4b668 @name="Germany", @code="DE">, 
#<Country:0x5e4a2a0 @name="Anguilla", @code="AI">, 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">
]

This is the same, whether or not I do .uniq and you can see there is two "Anguilla"


回答1:


Objects in array are considered duplicate by Array#uniq if their #hash values are duplicate, which is not the case in this code. You need to use different approach to do what intended, like this:

def countries_ive_drunk
  had_drinks.map {|drink| drink.beer.country.code }
    .uniq
    .map { |code| Country.get code}
end



回答2:


As pointed out by others, the problem is that uniq uses hash to distinguish between countries and that by default, Object#hash is different for all objects. It will also use eql? in case two objects return the same hash value, to be sure if they are eql or not.

The best solution is to make your class correct in the first place!

class Country
  # ... your previous code, plus:

  include Comparable

  def <=>(other)
    return nil unless other.is_a?(Country)
    (code <=> other.code).nonzero? || (name <=> other.name)
    # or less fancy:
    #   [code, name] <=> [other.code, other.name]
  end

  def hash
    [name, code].hash
  end

  alias eql? ==
end

Country.new("Canada", "CA").eql?(Country.new("Canada", "CA")) # => true

Now you can sort arrays of Countries, use countries as key for hashes, compare them, etc...

I've included the above code to show how it's done in general, but in your case, you get all this for free if you subclass Struct(:code, :name)...

class Country < Stuct(:name, :code)
  # ... the rest of your code, without the `attr_accessible` nor the `initialize`
  # as Struct provides these and `hash`, `eql?`, `==`, ...
end



回答3:


This boils down to what does equality mean? When is an object a duplicate of another? The default implementations of ==, eql? just compare the ruby object_id which is why you don't get the results you want.

You could implement ==, eql? and hash in a way that makes sense for your class, for example by comparing the countries' codes.

An alternative is to use uniq_by. This is an active support addition to Array, but mongoid depends on active support anyway, so you wouldn't be adding a dependency.

some_list_of_countries.uniq_by {|c| c.code}

Would use countries' codes to uniq them. You can shorten that to

 some_list_of_countries.uniq_by(&:code)



回答4:


Each element in the array is separate class instance.

#<Country:0x5e4a2a0 @name="Anguilla", @code="AI"> 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">

The ids are unique.




回答5:


#<Country:0x5e4a2a0 @name="Anguilla", @code="AI">, 
#<Country:0x5e48c98 @name="Anguilla", @code="AI">

Array#uniq thinks these are different objects (different instances of Country class), because the objects' ids are different. Obviously you need to change your strategy.




回答6:


At least as early as 1.9.3, Array#uniq would take a block just like uniq_by. uniq_by is now deprecated.



来源:https://stackoverflow.com/questions/8778354/uniq-of-ruby-array-fails-to-work

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!