3

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?

sawa
  • 165,429
  • 45
  • 277
  • 381
Eneroth3
  • 919
  • 8
  • 13
  • 1
    For code style, I'd recommend using `group_by` with a block over redefining `==`, but this is an interesting problem anyways. – thesecretmaster Aug 13 '18 at 13:56
  • I'm using the SketchUp Ruby API that already implements == or points, lengths and other classes. Also I'm not sure how a block could be used for this. The block has no access to the already defined groups or previous elements to compare against. For trivial cases like floats you could of course round to 3 decimals or so, but what about other classes? – Eneroth3 Aug 13 '18 at 14:07
  • I hope my answer helps you out here, TL;DR under the hood it's creating a hash, and the output of the block becomes the key to the hash. – thesecretmaster Aug 13 '18 at 14:11
  • If the sketchup API implements a `to_h` method or some way of converting element to and from a hash, you could `group_by(&:to_h)`. – thesecretmaster Aug 13 '18 at 14:19
  • In general, include [Comparable](https://ruby-doc.org/core-2.4.0/Comparable.html) in your class and (re)define `<=>` – steenslag Aug 13 '18 at 14:48
  • In this case, not all is clear. if 1.0 == 1.00001 == 1.00002 == 1.00003 then 1.0 == any number – steenslag Aug 13 '18 at 14:51
  • 2
    @steenslag Here the question is about hash comparison, which uses `eql?` which `Comparable` does not touch (AFAIK). So including `Comparable` would not help here. – thesecretmaster Aug 13 '18 at 15:11

2 Answers2

3

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) }
thesecretmaster
  • 1,950
  • 1
  • 27
  • 39
1

Group By uses hash and eql? to determine if a key inside the Hash is the same. So something like this should work:

class MyClass


 attr_accessor :value

  def initialize(value)
    @value = value
  end

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

  def hash
    rounded_value.hash
  end

  def eql?(other)
    self == other
  end

  def rounded_value
    @value.round(3)
  end
end

Please check if this gives you the required precission when marking keys as equal. Otherwise adapt the rounded_value method.

Here is a good summary of the equals operators/methods in ruby: What's the difference between equal?, eql?, ===, and ==?

Pascal
  • 8,464
  • 1
  • 20
  • 31