Using dynamic hash with strings and%

Ruby's formatting operator %

lets you use a hash to replace template values:

"%{a}" % { a: "banana" } # => "banana"

      

However, this doesn't work for dynamic hashes:

"%{a}" % Hash.new { |hash, key| hash[key] = "banana" } # => KeyError

      

Is there a workaround for this?

Edit: X for this Y creates a format hash from an array of possible values ​​for each key. My current solution looks something like this:

content = Hash[CONTENT.map { |k, v| [k, v.sample] }]

      

However, I think this is pretty ugly and would be better than a dynamic solution.

+3


source to share


2 answers


I don't think you can do what you want, at least not with an MRI.

If you follow through the code, you will find that it String#%

ends in C in sprintf.c

. %{x}

the processing part of the C implementation does this :

if (sym != Qnil) nextvalue = rb_hash_lookup2(hash, sym, Qundef);
if (nextvalue == Qundef) {
    rb_enc_raise(enc, rb_eKeyError, "key%.*s not found", len, start);
}

      

The call is rb_hash_lookup2

used to get the value of a key sym

from hash

and then KeyError

raises if rb_hash_lookup2

it finds nothing (i.e. rb_hash_lookup2

returns Qundef

). Now, if we look at rb_hash_lookup2

, we can see that it clearly does not use the default Hash:

VALUE
rb_hash_lookup2(VALUE hash, VALUE key, VALUE def)
{
    st_data_t val;

    if (!RHASH(hash)->ntbl || !st_lookup(RHASH(hash)->ntbl, key, &val)) {
        return def; /* without Hash#default */
    }
    return (VALUE)val;
}

      

Note that it uses the argument def

and not the Hash default if it cannot find the key:



return def; /* without Hash#default */

      

If you look at the Hash#[]

implementation
, you can see that the only difference between it rb_hash_lookup2

is what return def;

is replaced by:

return hash_default_value(hash, key);

      

There's your problem: String#%

Explicitly bypassing your default Hash logic. And since all this is happening with direct calls to C, you can not get a patch monkey Hash#[]

, Hash#fetch

, Hash#has_key?

or something else; similarly, you cannot even subclass Hash and override any methods to kill the brain String#%

.

I think your ugly solution is less ugly than the alternatives (overriding String#%

, cracking the regex mess, ...).

YMMV with other Ruby implementations, of course.

0


source


You can use Hash # values_at to explicitly initialize keys for a string, but this is kind of a hack:



"%{a}, %{b}" % Hash.new {|h, k| h[k] = "banana" }.tap {|h| h.values_at(:a, :b) }

      

0


source







All Articles