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.
source to share
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.
source to share