Index: hash.c =================================================================== --- hash.c (revision 46485) +++ hash.c (working copy) @@ -2450,6 +2450,139 @@ return Qfalse; } +VALUE +map_hash(VALUE hash, int (*func)(ANYARGS)) +{ + VALUE result; + + RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); + result = rb_hash_new(); + if (!RHASH_EMPTY_P(hash)) { + rb_hash_foreach(hash, func, result); + } + return result; +} + +static int +map_keys_i(VALUE key, VALUE value, VALUE result) +{ + rb_hash_aset(result, rb_yield(key), value); + return ST_CONTINUE; +} + +/* + * call-seq: + * hsh.map_keys {| key | block } -> a_hash + * hsh.map_keys -> an_enumerator + * + * Returns a new hash, with the keys computed from running block + * once for each key in the hash, and the values unchanged. + * + * If no block is given, an enumerator is returned instead. + * + * h = { "a" => 100, "b" => 200 } + * h.map_keys { |key| key + '!' } #=> { "a!" => 100, "b!" => 200 } + * + */ + +VALUE +rb_hash_map_keys(VALUE hash) +{ + return map_hash(hash, map_keys_i); +} + +/* + * call-seq: + * hsh.map_keys! {| key | block } -> hsh or nil + * hsh.map_keys! -> an_enumerator + * + * Replaces every key in the hash with the result of calling block + * once for each key in the hash. Returns nil if no changes were made. + * + * If no block is given, an enumerator is returned instead. + * + * h = { "a" => 100, "b" => 200 } + * h.map_keys! { |key| key + '!' } #=> { "a!" => 100, "b!" => 200 } + * + */ + +VALUE +rb_hash_map_keys_bang(VALUE hash) +{ + st_index_t n; + VALUE keys; + long i; + + RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); + rb_hash_modify(hash); + n = RHASH_SIZE(hash); + if (!n) return Qnil; + + keys = rb_hash_keys(hash); + for (i = 0; i < RARRAY_LEN(keys); i++) { + VALUE key = RARRAY_AREF(keys, i); + rb_hash_aset(hash, rb_yield(key), rb_hash_delete(hash, key)); + } + + return hash; +} + +static int +map_values_i(VALUE key, VALUE value, VALUE result) +{ + rb_hash_aset(result, key, rb_yield(value)); + return ST_CONTINUE; +} + +/* + * call-seq: + * hsh.map_values {| value | block } -> a_hash + * hsh.map_values -> an_enumerator + * + * Returns a new hash, with the values computed from running block + * once for each value in the hash, and the values unchanged. + * + * If no block is given, an enumerator is returned instead. + * + * h = { "a" => 100, "b" => 200 } + * h.map_values { |value| value * 2 } #=> { "a" => 200, "b" => 400 } + * + */ + +VALUE +rb_hash_map_values(VALUE hash) +{ + return map_hash(hash, map_values_i); +} + +/* + * call-seq: + * hsh.map_values! {| value | block } -> hsh or nil + * hsh.map_values! -> an_enumerator + * + * Replaces every value in the hash with the result of calling block + * once for each value in the hash, and the keys unchanged. + * + * If no block is given, an enumerator is returned instead. + * + * h = { "a" => 100, "b" => 200 } + * h.map_values! { |value| value * 2 } #=> { "a" => 200, "b" => 400 } + * + */ + +VALUE +rb_hash_map_values_bang(VALUE hash) +{ + st_index_t n; + + RETURN_SIZED_ENUMERATOR(hash, 0, 0, hash_enum_size); + n = RHASH_SIZE(hash); + if (!n) return Qnil; + rb_hash_foreach(hash, map_values_i, hash); + + return hash; +} + static int path_tainted = -1; static char **origenviron; @@ -3810,6 +3943,10 @@ rb_define_method(rb_cHash, "assoc", rb_hash_assoc, 1); rb_define_method(rb_cHash, "rassoc", rb_hash_rassoc, 1); rb_define_method(rb_cHash, "flatten", rb_hash_flatten, -1); + rb_define_method(rb_cHash, "map_keys", rb_hash_map_keys, 0); + rb_define_method(rb_cHash, "map_keys!", rb_hash_map_keys_bang, 0); + rb_define_method(rb_cHash, "map_values", rb_hash_map_values, 0); + rb_define_method(rb_cHash, "map_values!", rb_hash_map_values_bang, 0); rb_define_method(rb_cHash,"include?", rb_hash_has_key, 1); rb_define_method(rb_cHash,"member?", rb_hash_has_key, 1); Index: test/ruby/test_hash.rb =================================================================== --- test/ruby/test_hash.rb (revision 46485) +++ test/ruby/test_hash.rb (working copy) @@ -1269,6 +1269,55 @@ assert_equal(bug9381, hash[wrapper.new(5)]) end + def test_map_keys + original = { 'a' => 'a', 'b' => 'b' } + mapped = original.map_keys { |key| key + '!' } + + assert_equal({ 'a' => 'a', 'b' => 'b' }, original) + assert_equal({ 'a!' => 'a', 'b!' => 'b' }, mapped) + end + + def test_map_keys_bang + original = { 'a' => 'a', 'b' => 'b' } + mapped = original.map_keys! { |key| key + '!' } + + assert_equal({ 'a!' => 'a', 'b!' => 'b' }, original) + assert_equal({ 'a!' => 'a', 'b!' => 'b' }, mapped) + end + + def test_map_keys_bang_on_empty_hash + original = {} + mapped = original.map_keys! { |key| key + '!' } + + assert_equal({}, original) + assert_nil mapped + end + + def test_map_values + original = { 'a' => 'a', 'b' => 'b' } + mapped = original.map_values { |value| value + '!' } + + assert_equal({ 'a' => 'a', 'b' => 'b' }, original) + assert_equal({ 'a' => 'a!', 'b' => 'b!' }, mapped) + end + + def test_map_values_bang + original = { 'a' => 'a', 'b' => 'b' } + mapped = original.map_values! { |value| value + '!' } + + assert_equal({ 'a' => 'a!', 'b' => 'b!' }, original) + assert_equal({ 'a' => 'a!', 'b' => 'b!' }, mapped) + end + + def test_map_values_bang_on_empty_hash + original = {} + mapped = original.map_values! { |value| value + '!' } + + assert_equal({}, original) + assert_nil mapped + end + + class TestSubHash < TestHash class SubHash < Hash def reject(*)