Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to group numbers into different buckets in Ruby

Tags:

ruby

I have a file with numbers on each line:

0101
1010
1311
0101
1311
431
1010
431
420

I want have a hash with the number of occurrences of each number, in this case:

{0101 => 2, 1010 => 2, 1311 => 2, 431 => 2, 420 => 1}

How can I do this?

like image 521
josh Avatar asked Nov 29 '10 01:11

josh


3 Answers

Simple one-liner, given an array items:

items.inject(Hash.new(0)) {|hash, item| hash[item] += 1; hash}

How it works:

Hash.new(0) creates a new Hash where accessing undefined keys returns 0.

inject(foo) iterates through an array with the given block. For the first iteration, it passes foo, and on further iterations, it passes the return value of the last iteration.

Another way to write it would be:

hash = Hash.new(0)
items.each {|item| hash[item] += 1}
like image 197
Chuck Avatar answered Oct 07 '22 05:10

Chuck


This is essentially the same as Chuck's, but when you are creating an array or hash, 'each_with_object' will make it slightly simpler than 'inject', as you do not have to write the final array or hash in the block.

items.each_with_object(Hash.new(0)) {|item, hash| hash[item] += 1}
like image 38
sawa Avatar answered Oct 07 '22 04:10

sawa


ID = -> x { x } # Why is the identity function not in the core lib?

f = <<-HERE
  0101
  1010
  1311
  0101
  1311
  431
  1010
  431
  420
HERE

Hash[f.lines.map(&:to_i).group_by(&ID).map {|n, ns| [n, ns.size] }]
# { 101 => 2, 1010 => 2, 1311 => 2, 431 => 2, 420 => 1 }

You simply group the numbers by themselves using Enumerable#group_by, which gives you something like

{ 101 => [101, 101], 420 => [420] }

And then you Enumerable#map the value arrays to their lengths, i.e. [101, 101] becomes 2. Then just convert it back to a Hash using Hash::[].

However, if you are willing to use a third-party library, it becomes even more trivial, because if you use a MultiSet data structure, the answer falls out naturally. (A MultiSet is like a Set, except that an item can be added multiple times and the MultiSet will keep count of how often an item was added – which is exactly what you want.)

require 'multiset' # Google for it, it's so old that it isn't available as a Gem

Multiset[*f.lines.map(&:to_i)]
# => #<Multiset:#2 101, #2 1010, #2 1311, #2 431, #1 420>

Yes, that's it.

That's the beautiful thing about using the right data-structure: your algorithms become massively simpler. Or, in this particular case, the algorithm just vanishes.

I've written more about using MultiSets for solving this exact problem at

  • find the repetition of duplicate numbers. (the first example in that post is the same as the second one here, the second example in that post is essentially the same as @Chuck's here and the third one is the same as the first in this post.)
  • Counting occurrences of unique letters in a string (the Ruby example is the same as here, the Scala and ECMAScript examples are the same as @Chuck's and the C# example is the same as my group_by example here.)
  • Bag/Multiset in scala and a touch of multimap (Again the same three examples as my two and @Chuck's in Scala.)
like image 29
Jörg W Mittag Avatar answered Oct 07 '22 06:10

Jörg W Mittag