Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby comparison used by 'group_by'

Tags:

ruby

group-by

I want to group objects that are virtually the same but not identical like in this case:

class MyClass
  attr_accessor :value

  def initialize(value)
    @value = value
  end

  def ==(other)
    (@value - other.value).abs < 0.0001
  end
end

With the precision relevant for my implementation, two values differing by 0.0001 can be regarded identical:

MyClass.new(1.0) == MyClass.new(1.00001)
# => true

I want these to be in the same group:

[MyClass.new(1.0), MyClass.new(1.00001)].group_by(&:value)
# => {1.0=>[#<MyClass:0x0000000d1183e0 @value=1.0>], 1.00001=>[#<MyClass:0x0000000d118390 @value=1.00001>]}

What comparison is used for group_by? Can the built in group_by be made to honor the custom == method, or is a custom group_by method required for this?

like image 309
Eneroth3 Avatar asked Dec 17 '25 13:12

Eneroth3


1 Answers

TL;DR It appears to me that this issue is because group_by doesn't actually check equality anywhere. It generates the hash, and uses the elements of the array as keys.

Long story:

My first guess here was that it was doing something like my_arr.map(&:value).group_by { |i| i }, which would mean that it would be checking the equality of 2 floats instead of 2 MyClasses. To test this, I redefined == on a float, and added debugging puts statements to both of our definitions of ==. Interestingly, nothing was printed. So, I went on over to the documentation for group_by, and looked at the source code:

               static VALUE
enum_group_by(VALUE obj)
{
    VALUE hash;

    RETURN_SIZED_ENUMERATOR(obj, 0, 0, enum_size);

    hash = rb_hash_new();
    rb_block_call(obj, id_each, 0, 0, group_by_i, hash);
    OBJ_INFECT(hash, obj);

    return hash;
}

Notice the last argument to rb_block_call -- It's a hash. This hinted to me that under the hood, ruby is doing this:

def group_by(&:block)
  h = {}
  self.each do |ele|
    key = block_given? ? block.call(ele) : ele
    h[key] ||= []
    h[key].push(ele)
  end
end

When getting a key from a hash, it seems that == is not called, and so this attempt to redefine == didn't do what you wanted. A way to fix this would be like so:

[MyClass.new(1.0), MyClass.new(1.00001)].group_by { |i| i.value.round(2) }
like image 184
thesecretmaster Avatar answered Dec 20 '25 09:12

thesecretmaster



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!