sawa sawa - 2 months ago 17
Ruby Question

Does Enumerable's group_by preserve the Enumerable's order?

Does

preserve the original order within each value? When I get this:

[1, 2, 3, 4, 5].group_by{|i| i % 2}
# => {1=>[1, 3, 5], 0=>[2, 4]}


is it guaranteed that, for example, the array
[1, 3, 5]
contains the elements in this order and not, for example
[3, 1, 5]
?

Is there any description regarding this point?

I am not mentioning the order between the keys
1
and
0
. That is a different issue.

Answer

Yes, Enumerable#group_by preserves input order.

Here's the implementation of that method in MRI, from https://github.com/ruby/ruby/blob/trunk/enum.c:

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;
}

static VALUE
group_by_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, hash))
{
    VALUE group;
    VALUE values;

    ENUM_WANT_SVALUE();

    group = rb_yield(i);
    values = rb_hash_aref(hash, group);
    if (!RB_TYPE_P(values, T_ARRAY)) {
        values = rb_ary_new3(1, i);
        rb_hash_aset(hash, group, values);
    }
    else {
        rb_ary_push(values, i);
    }
    return Qnil;
}

enum_group_by calls group_by_i on each array (obj) element in order. group_by_i creates a one-element array (rb_ary_new3(1, i)) the first time a group is encountered, and pushes on to the array thereafter (rb_ary_push(values, i)). So the input order is preserved.

Also, RubySpec requires it. From https://github.com/rubyspec/rubyspec/blob/master/core/enumerable/group_by_spec.rb:

it "returns a hash with values grouped according to the block" do
  e = EnumerableSpecs::Numerous.new("foo", "bar", "baz")
  h = e.group_by { |word| word[0..0].to_sym }
  h.should == { :f => ["foo"], :b => ["bar", "baz"]}
end
Comments