Изменение каждого значения в хэше в Ruby

170

Я хочу изменить каждое значение в хэше, чтобы добавить «%» до и после значения так

{ :a=>'a' , :b=>'b' }

должен быть изменен на

{ :a=>'%a%' , :b=>'%b%' }

Какой лучший способ сделать это?

theReverseFlick
источник
1
Пожалуйста, уточните, если вы хотите изменить исходные строковые объекты, просто изменить исходные объекты или ничего не изменять.
Phrogz
1
Тогда вы приняли неправильный ответ. (Не обижайтесь на @pst, поскольку я лично также выступаю за программирование в функциональном стиле вместо мутирования объектов.)
Phrogz
Но все же подход был очень приятным
theReverseFlick

Ответы:

178

Если вы хотите, чтобы сами фактические строки мутировали на месте (возможно и желательно влияя на другие ссылки на те же строковые объекты):

# Two ways to achieve the same result (any Ruby version)
my_hash.each{ |_,str| str.gsub! /^|$/, '%' }
my_hash.each{ |_,str| str.replace "%#{str}%" }

Если вы хотите, чтобы хеш изменился на месте, но не хотите влиять на строки (вы хотите, чтобы он получал новые строки):

# Two ways to achieve the same result (any Ruby version)
my_hash.each{ |key,str| my_hash[key] = "%#{str}%" }
my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }

Если вы хотите новый хеш:

# Ruby 1.8.6+
new_hash = Hash[*my_hash.map{|k,str| [k,"%#{str}%"] }.flatten]

# Ruby 1.8.7+
new_hash = Hash[my_hash.map{|k,str| [k,"%#{str}%"] } ]
Phrogz
источник
1
@ Эндрю Маршалл Правильно, спасибо. В Ruby 1.8 Hash.[]не принимает массив пар массивов, ему требуется четное количество прямых аргументов (отсюда и сплат).
Phrogz
2
На самом деле, Hash. [Key_value_pairs] был введен в 1.8.7, поэтому только Ruby 1.8.6 не нуждается в сплате и выравнивании.
Марк-Андре Лафортун
2
@Aupajo возвращает Hash#eachключ и значение для блока. В данном случае меня не заботил ключ, и я не назвал его полезным. Имена переменных могут начинаться с подчеркивания, а на самом деле могут быть только подчеркиванием. В этом нет никакого выигрыша в производительности, это всего лишь тонкое самодокументированное примечание, что я ничего не делаю с этим первым значением блока.
Phrogz
1
Я думаю, что вы имеете в виду my_hash.inject(my_hash){ |h,(k,str)| h[k]="%#{str}%"; h }, должны вернуть хэш из блока
aceofspades
1
В качестве альтернативы вы можете использовать метод each_value, который немного проще для понимания, чем использование подчеркивания для значения неиспользованного ключа.
Стрэнд Маккатчен
266

В Ruby 2.1 и выше вы можете сделать

{ a: 'a', b: 'b' }.map { |k, str| [k, "%#{str}%"] }.to_h
shock_one
источник
5
Спасибо, на самом деле было то, что я искал. Не знаю, почему за тебя так мало проголосовали.
simperreault
7
Это, однако, очень медленно и очень требовательно к оперативной памяти. Входной Hash итерируется для получения промежуточного набора вложенных массивов, которые затем преобразуются в новый Hash. Игнорируя пиковое использование ОЗУ, время выполнения намного хуже - сравнение этого с решениями для модификации на месте в другом ответе показывает 2,5 с 1,5 с за то же число итераций. Поскольку Ruby является сравнительно медленным языком, избегать медленных кусочков медленного языка имеет большой смысл :-)
Эндрю Ходжкинсон
2
@AndrewHodgkinson, хотя в целом я согласен и не выступаю за то, чтобы не обращать внимания на производительность во время выполнения, разве отслеживание всех этих ловушек производительности не становится болью и не идет вразрез с философией ruby ​​о «производительности сначала разработчиков»? Я думаю, что это не комментарий для вас, а скорее общий комментарий к возможному парадоксу, к которому мы приводим использование рубина.
elsurudo
4
Проблема в том, что мы уже отказались от производительности, приняв решение даже использовать ruby, так что же изменит этот «маленький кусочек»? Это скользкий склон, не так ли? Для протокола, я предпочитаю это решение принятому ответу с точки зрения читабельности.
elsurudo
1
Если вы используете Ruby 2.4+, его еще проще использовать, #transform_values!как указано в sschmeck ( stackoverflow.com/a/41508214/6451879 ).
Финн
128

Ruby 2.4 представил метод Hash#transform_values!, который вы можете использовать.

{ :a=>'a' , :b=>'b' }.transform_values! { |v| "%#{v}%" }
# => {:a=>"%a%", :b=>"%b%"} 
sschmeck
источник
Именно то, что я искал!
Dolev
4
Конечно, есть Hash#transform_values(без взрыва), который не изменяет приемник. В противном случае отличный ответ, спасибо!
iGEL
1
Это действительно уменьшит мое использование reduce:-p
iGEL
86

Лучший способ изменить значения хэша на месте

hash.update(hash){ |_,v| "%#{v}%" }

Меньше кода и четких намерений. Также быстрее, потому что никакие новые объекты не выделяются за пределы значений, которые должны быть изменены.

Сим
источник
Не совсем верно: новые строки выделены. Тем не менее, интересное решение, которое эффективно. +1
Phrogz
@Phrogz хорошая точка зрения; Я обновил ответ. В общем случае нельзя избежать выделения значений, поскольку не все преобразования значений могут быть выражены в виде мутаторов, таких как gsub!.
Сим
3
Так же, как мой ответ, но с другим синонимом, я согласен, что updateпередает намерение лучше, чем merge!. Я думаю, что это лучший ответ.
1
Если вы не используете k, используйте _вместо этого.
sekrett
28

Чуть более читаемый, mapэто массив одноэлементных хешей и reduceчто сmerge

the_hash.map{ |key,value| {key => "%#{value}%"} }.reduce(:merge)
edgerunner
источник
2
Hash[the_hash.map { |key,value| [key, "%#{value}%"] }]
Более понятное
Это крайне неэффективный способ обновления значений. Для каждой пары значений сначала создается пара Array(для map), а затем a Hash. Затем каждый шаг операции уменьшения будет дублировать «памятку» Hashи добавлять новую пару ключ-значение. По крайней мере , использование :merge!в reduceмодифицировать окончательное Hashна месте. И, в конце концов, вы не изменяете значения существующего объекта, а создаете новый объект, что не является вопросом вопроса.
Сим
он возвращается, nilесли the_hashпуст
DNNX
18

Для этой задачи существует новый метод «Rails way» :) http://api.rubyonrails.org/classes/Hash.html#method-i-transform_values

woto
источник
8
Также Ruby 2.4.0+ содержит Hash#transform_values. Это должно быть способом идти с этого момента.
Amoebe
1
В двух словах, доступно для Rails 5+ или Ruby 2.4+
Кирилл Дюшон-Дорис
16

Один метод, который не вводит побочные эффекты к оригиналу:

h = {:a => 'a', :b => 'b'}
h2 = Hash[h.map {|k,v| [k, '%' + v + '%']}]

Hash # map также может быть интересным, поскольку объясняет, почему Hash.mapне возвращает Hash (именно поэтому результирующий массив [key,value]пар преобразуется в новый Hash) и предоставляет альтернативные подходы к тому же общему шаблону.

Удачного кодирования.

[Отказ от ответственности: я не уверен, если Hash.map семантика изменяется в Ruby 2.x]


источник
7
Знает ли Matz, что Hash.mapсемантика меняется в Ruby 2.x?
Эндрю Гримм
1
Метод Hash # [] так полезен, но так ужасен. Есть ли более красивый способ преобразования массивов в хэши таким же образом?
Мартин
15
my_hash.each do |key, value|
  my_hash[key] = "%#{value}%"
end
Эндрю Маршалл
источник
Я не люблю побочные эффекты, но +1 за подход :) Есть each_with_objectв Ruby 1.9 (IIRC), который избегает необходимости прямого доступа к имени и Map#mergeтакже может работать. Не уверен, как отличаются сложные детали.
1
Первоначальный хэш изменен - ​​это нормально, если поведение ожидается, но может вызвать тонкие проблемы, если «забыто». Я предпочитаю уменьшить изменчивость объекта, но это не всегда может быть практичным. (Ruby вряд ли является языком без побочных эффектов ;-)
8

Hash.merge! самое чистое решение

o = { a: 'a', b: 'b' }
o.merge!(o) { |key, value| "%#{ value }%" }

puts o.inspect
> { :a => "%a%", :b => "%b%" }
Дориан
источник
@MichieldeMare Извините, я недостаточно тщательно это проверил. Вы правы, блок должен принимать два параметра. Исправлено.
5

После тестирования с RSpec вот так:

describe Hash do
  describe :map_values do
    it 'should map the values' do
      expect({:a => 2, :b => 3}.map_values { |x| x ** 2 }).to eq({:a => 4, :b => 9})
    end
  end
end

Вы можете реализовать Hash # map_values ​​следующим образом:

class Hash
  def map_values
    Hash[map { |k, v| [k, yield(v)] }]
  end
end

Затем функцию можно использовать так:

{:a=>'a' , :b=>'b'}.map_values { |v| "%#{v}%" }
# {:a=>"%a%", :b=>"%b%"}
wedesoft
источник
1

Если вам интересно, какой вариант на месте является самым быстрым, то здесь:

Calculating -------------------------------------
inplace transform_values! 1.265k  0.7%) i/s -      6.426k in   5.080305s
      inplace update      1.300k  2.7%) i/s -      6.579k in   5.065925s
  inplace map reduce    281.367   1.1%) i/s -      1.431k in   5.086477s
      inplace merge!      1.305k  0.4%) i/s -      6.630k in   5.080751s
        inplace each      1.073k  0.7%) i/s -      5.457k in   5.084044s
      inplace inject    697.178   0.9%) i/s -      3.519k in   5.047857s
lzap
источник