From: Michael Edgar Date: 2011-03-28T04:50:21+09:00 Subject: [ruby-core:35564] Re: [feature:trunk] Enumerable#categorize This looks astounding. Quick nit: is it #categorize or #associate? I like #categorize as a name for this more, but you've given code samples with #associate as the working title of the method. I really like what you've presented here. I have one idea for how to handle varying key lengths: I think I'd like an option to (or the default case to be, even, but at least an option) have a replacing, mixed-mode result: both values and mixed nesting is allowed, and you can replace a value with a nesting level if key duplication occurs. Example where the categories are fruits: [ [:aaa, "plum"], [:aaa, :bbb, "banana"], [:aaa, :ccc, "lemon"], [:foo, :bar, "pear"], [:foo, "apple"], [:zzz, "orange" ] ].categorize { |a| a } should give: {:aaa => {:bbb => "banana", :ccc => "lemon" }, :foo => "apple" }, :zzz => "orange" } It's a neat way to provide a useful result, as well as to avoid the overhead of having to actually check for this... though, the overhead is asymptotically negligible. Just food for thought. Cheers, Michael Edgar adgar@carboni.ca http://carboni.ca/ On Mar 26, 2011, at 11:25 PM, Marc-Andre Lafortune wrote: > Following the comments of Akira and others, here's a revised proposal > merging his original Enumerable#categorize with my previous version. > > Like Akira's categorize, it now: > * can produce nested hashes > * returns an Enumerator with not given a block > > Like my original proposal, it still: > * has a simple interface with a single argument for special merges > * does not produce "grouped hashes" by default > > Here is what the documentation could read like: > > enum.associate(merge = nil){ block } # => a_hash > > Invokes block once for each element of +enum+. Creates a new hash based on > the values returned by the block. These values are interpreted as a sequence > of keys and the final value. > > (1..3).associate {|e| ["#{e} + #{e}", e+e] } > #=> {"1 + 1" => 2, "2 + 2" => 4, "3 + 3" => 6} > > If more than one key is specified, the resulting hash will be nested. > > (0..7).associate {|e| [e&4, e&2, e&1, e] } > #=> {0=>{0=>{0=>0, > 1=>1}, > 2=>{0=>2, > 1=>3}}, > 4=>{0=>{0=>4, > 1=>5}, > 2=>{0=>6, > 1=>7}}} > > If no key is specified, either because the block returned an array > with less than two elements, or because only the value is not an Array, > then the key is assumed to be the yielded element itself > (or the first element in case many elements are yielded): > > (1..4).associate{|i| i ** i} # => {1 => 1, 2 => 2, 3 => 27, 4 => 256} > {:foo => 2, :bar => 3}.associate{|k, v| v ** v} > # => {:foo => 4, :bar => 9} > > In case of key duplication, +merge+ will be used. If +nil+, the value > is overwritten. Otherwise the stored value will be the result of calling > `merge` with the arguments +key+, +first_value+ and +other_value+ > (see Hash#merge). In a similar way to `Enumerable#inject`, passing a symbol > for +merge+ is equivalent to passing > ->(key, first, other){ first.send(merge, other) } > > x = [[:foo, 10], [:bar, 30], [:foo, 32]] > x.associate{|e| e} # => {:foo => 32, :bar => 30} > x.associate(->(k, a, b){a}){|e| e} # => {:foo => 10, :bar => 30} > x.associate(:+){|e| e} # => {:foo => 42, :bar => 30} > x.associate(:concat){|k, v| [k, [v]]} # => {:foo => [10, 32], > :bar => [30]} > > > A question that remains is: should there be special checks for cases > where the result has varying length? > E.g., what error should the following raise (or what should be the result): > > [[:foo, :bar], [:foo, :bar, :baz]].associate{|x| x} # => ?? > > Here is what a Ruby implementation could look like: > > module Enumerable > def associate(merge_func = nil) > return to_enum, __method__, merge_func unless block_given? > > if merge_func.is_a? Symbol > sym = merge_func > merge_func = ->(k, v1, v2){v1.send(sym, v2)} > end > > top_level_hash = {} > each do |*elems| > result = yield(*elems) > result = [result] unless result.is_a? Array > value = result.pop > > if result.empty? # deduce key > key = elems.first > key = key.first if key.is_a?(Array) > initial_keys = [] > else > key = result.pop > initial_keys = result > end > > final_hash = initial_keys.inject(top_level_hash){|cur_h, k| > cur_h[k] ||= {}} > > if merge_func && final_hash.has_key?(key) > value = merge_func.call(key, final_hash[key], value) > end > final_hash[key] = value > end > top_level_hash > end > end > > > Thanks > -- > Marc-Andr� >