52

I am using Ruby on Rails 3.0.10 and I would like to build an hash key\value pairs in a conditional way. That is, I would like to add a key and its related value if a condition is matched:

hash = {
  :key1 => value1,
  :key2 => value2, # This key2\value2 pair should be added only 'if condition' is 'true'
  :key3 => value3,
  ...
}

How can I do that and keep a "good" readability for the code? Am I "forced" to use the merge method?

Russell
  • 12,261
  • 4
  • 52
  • 75
Backo
  • 18,291
  • 27
  • 103
  • 170
  • Where do the keys and values come from? A different hash? – Ray Toal Sep 07 '11 at 03:47
  • @Ray Toal - No from a different hash. I am building an hash from scratch. – Backo Sep 07 '11 at 03:51
  • 3
    I'm sorry I don't understand. The data has to come from somewhere: a string, another hash, an array, or someplace else. Even if you "build the hash from scratch" you have to have the data. Writing conditional insertions in a hash literal isn't done in Ruby; you can delete after the fact using Chris Jester-Young's technique. – Ray Toal Sep 07 '11 at 04:05
  • is there just one condition that controls all the conditional values? – Martin DeMello Sep 07 '11 at 05:46
  • @Martin DeMello - No. One condition is used in order to add or not just *one* key\value pairs. – Backo Sep 07 '11 at 05:48
  • 2
    @backo you mean there's exactly one conditional value in the entire hash? if so i'd just say insert it at the end – Martin DeMello Sep 07 '11 at 05:56

11 Answers11

69

I prefer tap, as I think it provides a cleaner solution than the ones described here by not requiring any hacky deleting of elements and by clearly defining the scope in which the hash is being built.

It also means you don't need to declare an unnecessary local variable, which I always hate.

In case you haven't come across it before, tap is very simple - it's a method on Object that accepts a block and always returns the object it was called on. So to build up a hash conditionally you could do this:

Hash.new.tap do |my_hash|
  my_hash[:x] = 1 if condition_1
  my_hash[:y] = 2 if condition_2
  ...
end

There are many interesting uses for tap, this is just one.

Russell
  • 12,261
  • 4
  • 52
  • 75
  • 2
    Is the assignment missing? `hash = Hash.new.tap do ...` – tokland Feb 04 '14 at 10:10
  • 6
    It depends if you want to assign it to anything. For me, typically, this is more than enough for a method to do by itself, so this would just be the entire body of the method - no need to assign it to anything. – Russell Feb 05 '14 at 18:38
  • Is it possible to do an else condition in this as well? – Sankalp Singha Feb 10 '15 at 17:59
  • @SankalpSingha Yes, but it's not as pretty: Either using ternary expression like `condition_1 ? my_hash[:x1] : my_hash[:x2]` or `if condition_1 then my_hash[:x1] else my_hash[:x2] end` or even making it multiline will work. – Joshua Pinter Jan 16 '19 at 16:45
43

A functional approach with Hash.compact:

hash = {
  :key1 => 1,
  :key2 => (2 if condition),
  :key3 => 3,
}.compact 
tokland
  • 66,169
  • 13
  • 144
  • 170
14

Probably best to keep it simple if you're concerned about readability:

hash = {}
hash[:key1] = value1
hash[:key2] = value2 if condition?
hash[:key3] = value3
...
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
dwhalen
  • 1,927
  • 12
  • 11
  • I like `tap` but honestly, this is probably the way to go since it's not doing anything fancy or magical. It's the "lowest common denominator" solution. – Joshua Pinter Jan 16 '19 at 16:47
  • I'm coming back to this 8 months later and unbeknownst to me, I came to the same conclusion as my past self. Less magic the better. – Joshua Pinter Aug 08 '19 at 02:40
7

Keep it simple:

hash = {
  key1: value1,
  key3: value3,
}

hash[:key2] = value2 if condition

This way you also visually separate your special case, which might get unnoticed if it is buried within hash literal assignment.

coisnepe
  • 480
  • 8
  • 18
Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • 1
    This should be the accepted answer. All the other answers are either over engineered and/or harder on the eyes. – coisnepe May 13 '19 at 12:48
4

I use merge and the ternary operator for that situation,

hash = {
  :key1 => value1,
  :key3 => value3,
  ...
}.merge(condition ? {:key2 => value2} : {})
Dave Morse
  • 717
  • 9
  • 16
3

Simple as this:

hash = {
  :key1 => value1,
  **(condition ? {key2: value2} : {})
}

Hope it helps!

Dusht
  • 4,712
  • 3
  • 18
  • 24
1

In case you want to add few keys under single condition, you can use merge:

hash = {
  :key1 => value1,
  :key2 => value2,
  :key3 => value3
}

if condition
  hash.merge!(
    :key5 => value4,
    :key5 => value5,
    :key6 => value6
  )
end

hash
spirito_libero
  • 1,206
  • 2
  • 13
  • 21
1

IF you build hash from some kind of Enumerable data, you can use inject, for example:

raw_data.inject({}){ |a,e| a[e.name] = e.value if expr; a }
Dmitry Maksimov
  • 2,861
  • 24
  • 19
0

Using fetch can be useful if you're populating a hash from optional attributes somewhere else. Look at this example:

def create_watchable_data(attrs = {})
  return WatchableData.new({
    id:             attrs.fetch(:id, '/catalog/titles/breaking_bad_2_737'),
    titles:         attrs.fetch(:titles, ['737']),
    url:            attrs.fetch(:url, 'http://www.netflix.com/shows/breaking_bad/3423432'),
    year:           attrs.fetch(:year, '1993'),
    watchable_type: attrs.fetch(:watchable_type, 'Show'),
    season_title:   attrs.fetch(:season_title, 'Season 2'),
    show_title:     attrs.fetch(:id, 'Breaking Bad')
  })
end
Excalibur
  • 3,258
  • 2
  • 24
  • 32
0

First build your hash thusly:

hash = {
  :key1 => value1,
  :key2 => condition ? value2 : :delete_me,
  :key3 => value3
}

Then do this after building your hash:

hash.delete_if {|_, v| v == :delete_me}

Unless your hash is frozen or otherwise immutable, this would effectively only keep values that are present.

C. K. Young
  • 219,335
  • 46
  • 382
  • 435
-1

Same idea as Chris Jester-Young, with a slight readability trick

def cond(x)
  condition ? x : :delete_me
end

hash = {
  :key1 => value1,
  :key2 => cond(value2),
  :key3 => value3
}

and then postprocess to remove the :delete_me entries

Martin DeMello
  • 11,876
  • 7
  • 49
  • 64