tomasz74
tomasz74

Reputation: 16671

ruby do statements if hash key satisfies condition

I have:

event = {"first_type_a" => 100, "first_type_b" => false, "second_type_a" => "abc", "second_type_b" => false}

I am trying to change the keys of event based on the original. If the name contains a substring, the name will be changed, and the new key, pair value should be added, and the old pair should be removed. I expect to get:

event = {"important.a_1" => 100, "important.b_1" => false, "second_type_a" => "abc", "second_type_b" => false}

What would be the most efficient way to update event?

I expected this to work:

event.each_pair { |k, v|
  if !!(/first_type_*/  =~ k) do
        key = "#{["important", k.split("_", 3)[2]].join(".")}";
        event.merge!(key: v);
        event.delete(k)  
  end
}

but it raises an error:

  simpleLoop.rb:5: syntax error, unexpected keyword_do, expecting keyword_then or ';' or '\n'
  ... if !!(/first_type_*/  =~ k) do
  ...                             ^~
  simpleLoop.rb:9: syntax error, unexpected keyword_end, expecting '}'
    end
    ^~~
  simpleLoop.rb:21: embedded document meets end of file

I thought to approach it differently:

if result = event.keys.find { |k| k.include? "first_type_" } [
  key = "#{["important", k.split("_", 3)[2]].join(".")}"
  event.merge!(key: v)
  event.delete(k)
]

but still no luck. I am counting all brackets, and as the error indicates, it is something there, but I can't find it. Does the order matter?

Upvotes: 0

Views: 1192

Answers (3)

iGian
iGian

Reputation: 11193

You could define a method to be used inside the transform key call:

def transform(str)
  return ['important.', str.split('_').last, '_1'].join() if str[0..4] == 'first' # I checked just for "first"
  str
end

event.transform_keys! { |k| transform(k) } # Ruby >= 2.5 
event.map { |k, v| [transform(k), v] }.to_h # Ruby < 2.5 

Using Hash#each_pair but with Enumerable#each_with_object:

event.each_pair.with_object({}) { |(k, v), h| h[transform(k)] = v }

Or use as one liner:

event.transform_keys { |str| str[0..4] == 'first' ? ['important.', str.split('_').last, '_1'].join() : str  }

Upvotes: 1

Cary Swoveland
Cary Swoveland

Reputation: 110685

I will just show how this can be done quite economically in Ruby. Until recently, when wanting to modify keys of a hash one usually would do one of two things:

  • create a new empty hash and then add key/values pairs to the hash; or
  • convert the hash to an array a of two-element arrays (key-value pairs), modify the first element of each element of a (the key) and then convert a to the desired hash.

Recently (with MRI v2.4) the Ruby monks bestowed on us the handy methods Hash#transform_keys and Hash#transform_keys!. We can use the first of these profitably here. First we need a regular expression to match keys.

r = /
    \A           # match beginning of string
    first_type_  # match string
    (\p{Lower}+) # match 1+ lowercase letters in capture group 1
    \z           # match the end of the string
    /x           # free-spacing regex definition mode

Conventionally, this is written

r = /\Afirst_type_(\p{Lower}+)\z/

The use of free-spacing mode makes the regex self-documenting. We now apply the transform_keys method, together with the method String#sub and the regex just defined.

event = {"first_type_a"=>100, "first_type_b"=>false,
         "second_type_a"=>"abc", "second_type_b"=>false}

event.transform_keys { |k| k.sub(r, "important.#{'\1'}_1") }
  #=> {"important.a_1"=>100, "important.b_1"=>false,
  #    "second_type_a"=>"abc", "second_type_b"=>false} 

In the regex the p{} construct expression \p{Lower} could be replaced with \p{L}, the POSIX bracket expression [[:lower:]] (both match Unicode letters) or [a-z], but the last has the disadvantage that it will not match letters with diacritical marks. That includes letters of words borrowed from other languages that are used in English text (such as rosé, the wine). Search Regexp for documentation of POSIX and \p{} expressions.

If "first_type_" could be followed by lowercase or uppercase letters use \p{Alpha}; if it could be followed by alphanumeric characters, use \p{Alnum}, and so on.

Upvotes: 1

BookOfGreg
BookOfGreg

Reputation: 3716

The if starts a block, such as other structures if ... else ... end do ... end begin ... rescue ... end

Therefore your first example remove the do after the if, the block is already open. I also made it clearer by changing the block after each_pair to use do ... end rather than braces to help avoid confusing a hash with a block.

event = { 'first_type_a' => 100, 'first_type_b' => false, 'second_type_a' => 'abc', 'second_type_b' => false }
new_event = {}
event.each_pair do |k, v|
  if !!(/first_type_*/  =~ k)
    important_key = ['important', k.split('_', 3)[2]].join('.')
    new_event[important_key] = v
  else
    new_event[k] = v
  end
end

Upvotes: 1

Related Questions