Issue #4151 has been updated by trans (Thomas Sawyer).


I'm sorry, but #categorize has big code smell. Seems like too much functionality is being shoved into one method. It is still not clear to me after reading over explanation many times how #categorize works. So either it is relatively straight forward but the explanation is overly complexed, or the method itself is too complex. The later seems more likely given the number of options, especially lambda options, the method takes.

Let's look at a given example:

* [ruby-talk:288931]

  [["1", "01-02-2008", 5],
   ["1", "01-03-2008", 10],
   ["2", "12-25-2007", 5],
   ["1", "01-04-2008", 15]]
  to
  {"1" => {"01-02-2008" => 5, "01-03-2008" => 10, "01-04-2008" => 15},
   "2" => {"12-25-2007" => 5}}

Implemented as:

  orig.categorize(:op=>lambda {|x,y| y}) {|e| e }

Traditionally, the above would be something like:

  hash = Hash.new{|h,k| h[k]={}}
  list.each do |group, date, size|
    hash[group][date] ||= 0
    hash[group][date] += size
  end

This may be longer but it is very readable. Actually, if Ruby would *finally* offer some convenience method for the first line, e.g. `Hash.auto{{}}` instead of `Hash.new{|h,k| h[k]={}}`, then

  hash = Hash.auto{Hash.auto(0)}
  list.each{ |group, date, size| hash[group][date] += size }

I suspect almost every case for using #categorize will be able to be treated in much the same manner.

This is not to say however that I don't think some form(s) of `Enumerable -> Hash` would not be useful. I think it would, in fact I sometimes use Enumerable#mash (alias #graph).

    [1,2,3].mash{ |v| [v.to_s, v] }  #=> {'1'=>1, '2'=>2, '3'=>3}

But I would rather see a few methods that cover basic use cases that can work together and with other methods to build up more complex solutions then to create a single method that tries to cover every complex possibility in one go.


----------------------------------------
Feature #4151: Enumerable#categorize
https://bugs.ruby-lang.org/issues/4151#change-24918

Author: akr (Akira Tanaka)
Status: Open
Priority: Low
Assignee: akr (Akira Tanaka)
Category: 
Target version: 


=begin
 Hi.
 
 How about a method for converting enumerable to hash?
 
   enum.categorize([opts]) {|elt| [key1, ..., val] } -> hash
 
 categorizes the elements in _enum_ and returns a hash.
 
 The block is called for each elements in _enum_.
 The block should return an array which contains
 one or more keys and one value.
 
   p (0..10).categorize {|e| [e % 3, e % 5] }
   #=> {0=>[0, 3, 1, 4], 1=>[1, 4, 2, 0], 2=>[2, 0, 3]}
 
 The keys and value are used to construct the result hash.
 If two or more keys are provided
 (i.e. the length of the array is longer than 2),
 the result hash will be nested.
 
   p (0..10).categorize {|e| [e&4, e&2, e&1, e] }
   #=> {0=>{0=>{0=>[0, 8],
   #            1=>[1, 9]},
   #        2=>{0=>[2, 10],
   #            1=>[3]}},
   #    4=>{0=>{0=>[4],
   #            1=>[5]},
   #        2=>{0=>[6],
   #            1=>[7]}}}
 
 The value of innermost hash is an array which contains values for
 corresponding keys.
 This behavior can be customized by :seed, :op and :update option.
 
 This method can take an option hash.
 Available options are follows:
 
 - :seed specifies seed value.
 - :op specifies a procedure from seed and value to next seed.
 - :update specifies a procedure from seed and block value to next seed.
 
 :seed, :op and :update customizes how to generate
 the innermost hash value.
 :seed and :op behavies like Enumerable#inject.
 
 If _seed_ and _op_ is specified, the result value is generated as follows.
   op.call(..., op.call(op.call(seed, v0), v1), ...)
 
 :update works as :op except the second argument is the block value itself
 instead of the last value of the block value.
 
 If :seed option is not given, the first value is used as the seed.
 
   # The arguments for :op option procedure are the seed and the value.
   # (i.e. the last element of the array returned from the block.)
   r = [0].categorize(:seed => :s,
                      :op => lambda {|x,y|
                        p [x,y]               #=> [:s, :v]
                        1
                      }) {|e|
     p e #=> 0
     [:k, :v]
   }
   p r #=> {:k=>1}
 
   # The arguments for :update option procedure are the seed and the array
   # returned from the block.
   r = [0].categorize(:seed => :s,
                      :update => lambda {|x,y|
                        p [x,y]               #=> [:s, [:k, :v]]
                        1
                      }) {|e|
     p e #=> 0
     [:k, :v]
   }
   p r #=> {:k=>1}
 
 The default behavior, array construction, can be implemented as follows.
   :seed => nil
   :op => lambda {|s, v| !s ? [v] : (s << v) }
 
 Note that matz doesn't find satisfact in the method name, "categorize".
 [ruby-dev:42681]
 
 Also note that matz wants another method than this method,
 which the hash value is the last value, not an array of all values.
 This can be implemented by enum.categorize(:op=>lambda {|x,y| y}) { ... }.
 But good method name is not found yet.
 [ruby-dev:42643]
 -- 
 Tanaka Akira
 
 Attachment: enum-categorize.patch
=end



-- 
http://bugs.ruby-lang.org/