2

I have an array like that:

array = [{"id"=>"id1", "email"=>"name@organization.com", "sess"=>"sess1"},
{"id"=>"id2", "email"=>"name@organization.com", "sess"=>"sess2"},
{"id"=>"id3", "email"=>"name@organization.com", "sess"=>"sess2"},
{"id"=>"id4", "email"=>"name@organization.com", "sess"=>"sess3"},
{"id"=>"id5", "email"=>"name@organization.com", "sess"=>"sess2"},
{"id"=>"id6", "email"=>"name@organization.com", "sess"=>"sess3"},
{"id"=>"id7", "email"=>"name@organization.com", "sess"=>"sess2"},
{"id"=>"id8", "email"=>"name@organization.com", "sess"=>"sess5"},
{"id"=>"id9", "email"=>"name@organization.com", "sess"=>"sess2"},
{"id"=>"id10", "email"=>"name@organization.com", "sess"=>"sess2"},]

How can I do in a concise way something that returns all different occurrences of "sess" without repetitions?:

["sess1", "sess2", "sess3", "sess5"]

I've started to program a loop that iterates trough all elements and builds a new hash checking each time if the "sess" value is already present but I'm sure there must be a better way in Ruby.

Oerd
  • 2,256
  • 1
  • 21
  • 35
textypot
  • 23
  • 2

3 Answers3

6

Try that:

array.map{|n| n["sess"]}.uniq
joscas
  • 7,474
  • 5
  • 39
  • 59
  • If you have many (hundreds or thousands) of occurrences that map to just a handful distinct elements, your solution would take up a lot more memory then necessary (whereas a `Set` will only store the distinct elements) – Oerd Feb 06 '13 at 14:32
  • If the stored hashes always have a "sess" key it will be fine, otherweise you'll have some `nil` elements. A `.reject(&:nil?)` can solve that. – ichigolas Feb 06 '13 at 14:46
  • @nicooga that's right. What about `array.map{|n| n["sess"]}.uniq.compact` then? – joscas Feb 06 '13 at 14:51
  • 1
    @joscas: what I meand with memory requirements was that with map/uniq/compact you'd be juggling the complete array in memory a couple of times (but need only write a nice little line of code). Whereas when using a set, you'll need a couple more lines, but have an well defined upper limit for memory requirements. – Oerd Feb 06 '13 at 14:56
  • The temporary array uses a lot less memory than the input array, so I suppose the simple solution is best. But for the record, you can avoid temporary storage by simply doing: `array.inject({}) { |m, e| m[e['sess']] = :_; m }.keys` – DigitalRoss Feb 06 '13 at 15:11
  • 1
    BTW, the temporary structure created by map should only be using memory for the references ... the objects themselves aren't duplicated. – DigitalRoss Feb 06 '13 at 15:12
  • @DigitalRoss: yeah, storing references makes a lot more sense! Thanks a lot for clarifying! – Oerd Feb 06 '13 at 15:14
  • @Oerd +1 - What about this then?: `array.map{|n| n["sess"]}.to_set.to_a` – joscas Feb 06 '13 at 15:16
  • @joscas: the more, the merrier! I think I've gotten more out of this than the original poster :) +1 – Oerd Feb 06 '13 at 15:18
  • 1
    @Oerd: I don't think using sets is the conceptual answer to the memory-efficiency problem (though it's a practical solution, granted). The real problem is that map is strict (as oposed to lazy) and returns an array. What we'd want is to have better lazy data structures in the core that allowed, for example, `array.lazy.map { |h| h["sess"] }.uniq`. Ruby 2.0 fortunately goes this path. – tokland Feb 06 '13 at 15:26
  • @Marc-AndréLafortune Good point. This is only available in Rails I believe though. – joscas Feb 06 '13 at 19:36
  • Oups, I meant should use `array.uniq{|n| n["sess"]}`. See http://stackoverflow.com/questions/109781/uniq-by-object-attribute-in-ruby – Marc-André Lafortune Feb 06 '13 at 21:40
3

A simple #map followed by uniq (à la joscas) is a good solution, but just for fun, this will use minimum memory...

array.inject({}) { |m, e| m[e['sess']] = :_; m }.keys
Community
  • 1
  • 1
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
  • I think it's injecting `array['sess'] => array['sess']` elements to a *new* array and then (obviously) just getting the keys. (the last `m` just keeps the "accumulator" steady at `m` – Oerd Feb 06 '13 at 15:24
  • 1
    Probably this is simpler to undestand: `array.inject({}) { |acc, h| acc.update(h['sess'] => true) }.keys`. Also, when hashes are used as sets, well, it's probably better to use sets (just a require away). – tokland Feb 06 '13 at 15:41
  • @Marc-AndréLafortune I couldn't make your example work in the rails console. It gives all the keys, not just the 'sess' ones. – joscas Feb 06 '13 at 19:44
0

The data structure that stores unique occurrences of elements is called a Set. Ruby has a Set class in it's "standard library" and you can use it as shown in the snippet below (which you can then adapt to your code):

require 'set'

myset = Set.new
myset.reject(&:nil?)

myset.add('sess2')
myset.add('sess1')
myset.add('sess1')

Adding an item that already exists will have no apparent effect in a set. The previous snippet of code will result in a set composed of just 'sess1' and 'sess2' : <Set: {'sess1', 'sess2'}>

Also, as @nicooga pointed out a nil can be rejected if you don't need to include missing 'sess' keys.

Community
  • 1
  • 1
Oerd
  • 2,256
  • 1
  • 21
  • 35