user9869932
user9869932

Reputation: 7387

How could I get a nested hash value in a string interpolation in Ruby?

I have a nested hash in ruby like this

a = {
      'a': 1,
      'b': 2,
      'c': {
             'd': 3
           }
    }
=> {:a=>1, :b=>2, :c=>{:d=>3}}

and I set a.default = ''

How could I get the value of d if I use a string interpolation expression like:

puts "%{c['d']}" % a

I have unsuccessfully tried

puts "%{c}" % a
{:d=>3}
=> nil

puts "%{c['d']}" % a

=> nil

puts "%{c[:d]}" % a

=> nil

I would need some way to get the nested 3 in a['c']['d']. The two previous examples would suit me but they return empty string.

ps. If I don't use the a.default = '' I get the error

puts "%{c[:d]}" % a
KeyError: key{c[:d]} not found
from (pry):45:in `%'

**p.s: I'm using pry to run the code

Upvotes: 1

Views: 352

Answers (2)

Lam Phan
Lam Phan

Reputation: 3811

class String
  alias old_interpolation %
  def %(x)
    if x.is_a? Hash
      path = self.split('/').map(&:to_sym)
      begin
        x.dig(*path)
      rescue => error
      end
    else
      old_interpolation x
    end
  end
end

# input, note: no set `a.default = ''`
a = {
  'a': 1,
  'b': 2,
  'c': {
    'd': 3
  }
}
# output
puts "c" % a # {:d=>3}
puts "c/d" % a # 3
puts "a/d" % a # error -> return nil

You can change the separator '/' to what you want :D

update If you want to interpolate string, not just only get hash value

class String
  alias old_interpolation %
  def %(x)
    if x.is_a? Hash
      self.scan(/(?<=%{)[^}]*(?=})/).inject(self) do |result, match|
        keys = match.split('/').map(&:to_sym)
        begin
          value = x.dig(*keys)
        rescue => error
          # what should do here ?
        ensure
          result = result.sub(/%{[^}]*}/, value.to_s)
        end
        result
      end
    else
      old_interpolation x
    end
  end
end

puts "here %{c/d} we %{a} are" % a # here 3 we 1 are

Upvotes: 2

Ray Hamel
Ray Hamel

Reputation: 1309

I looked at the Ruby source code for sprintf and this does not appear to be possible. The relevant section is reproduced below.


      case '<':
      case '{':
        {
        const char *start = p;
        char term = (*p == '<') ? '>' : '}';
        int len;

        for (; p < end && *p != term; ) {
            p += rb_enc_mbclen(p, end, enc);
        }
        if (p >= end) {
            rb_raise(rb_eArgError, "malformed name - unmatched parenthesis");
        }
#if SIZEOF_INT < SIZEOF_SIZE_T
        if ((size_t)(p - start) >= INT_MAX) {
            const int message_limit = 20;
            len = (int)(rb_enc_right_char_head(start, start + message_limit, p, enc) - start);
            rb_enc_raise(enc, rb_eArgError,
                 "too long name (%"PRIuSIZE" bytes) - %.*s...%c",
                 (size_t)(p - start - 2), len, start, term);
        }
#endif
        len = (int)(p - start + 1); /* including parenthesis */
        if (sym != Qnil) {
            rb_enc_raise(enc, rb_eArgError, "named%.*s after <%"PRIsVALUE">",
                 len, start, rb_sym2str(sym));
        }
        CHECKNAMEARG(start, len, enc);
        get_hash(&hash, argc, argv);
        sym = rb_check_symbol_cstr(start + 1,
                       len - 2 /* without parenthesis */,
                       enc);
        if (!NIL_P(sym)) nextvalue = rb_hash_lookup2(hash, sym, Qundef);
        if (nextvalue == Qundef) {
            if (NIL_P(sym)) {
            sym = rb_sym_intern(start + 1,
                        len - 2 /* without parenthesis */,
                        enc);
            }
            nextvalue = rb_hash_default_value(hash, sym);
            if (NIL_P(nextvalue)) {
            rb_key_err_raise(rb_enc_sprintf(enc, "key%.*s not found", len, start), hash, sym);
            }
        }
        if (term == '}') goto format_s;
        p++;
        goto retry;
        }

Upvotes: 0

Related Questions