1
\$\begingroup\$

I've been trying to figure out a clean way of managing mappings between two objects. In the case of this example, it's two hashes. This spec should illustrate the problem at hand:

describe 'key mapping' do
  let(:have) { { data1: 'foo', data2: 'bar', data3: 'baz' } }
  let(:want) { { LINE1: 'foo', LINE2: 'bar', LINE3: 'baz' } }
  let(:result) { {} }

  after(:each) do
    expect(result).to eq want
  end

  it 'by assigning manually' do
    result[:LINE1] = have[:data1]
    result[:LINE2] = have[:data2]
    result[:LINE3] = have[:data3]
  end

  it 'by reading keys from a hash' do
    mappings = { data1: :LINE1, data2: :LINE2, data3: :LINE3 }
    mappings.each do |k, v|
      result[v] = have[k]
    end
  end
end

The notable thing here is that the source and destination can be mapped by known keys and unlike in the example there is no numeric correlation between the source and destination (example lists numbered keys just to make it easier to read)

The number of keys could be rather high, so the latter example will make code more readable, but is there an even better way of handling this?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

If there's no "logical" correlation between the key you have, and the key you want (i.e. no consistent way to rewrite them) then you basically have to use a lookup of some sort to do the translation.

So your "reading keys from a hash" strategy is your best bet, but it can be improved a little.

You can be more functional and just use map:

lookup = { data1: :LINE1, data2: :LINE2, data3: :LINE3 }

result = Hash[ lookup.map { |in, out| [out, have[in]] } ]

Or you can just go through the ones in the input hash, and translate the ones that are actually there, discarding ones without a translation:

result = Hash[ have.map { |k, v| [lookup[k], v] if lookup[k] }.compact ]

And for either of those, if you ever need to translate the other way, you can just use lookup.invert to flip the keys/values around.

Lastly, for you spec, don't put an expectation in a after(:each) block. Put the expectation in the spec itself.

\$\endgroup\$
6
  • \$\begingroup\$ I agree that using an array of arrays makes things more functional. I actually even considered using each_with_object, which would make things more readable. As for the expect in after block, that was just to simplify the example. Generally I avoid doing that. \$\endgroup\$
    – Ressu
    May 2, 2014 at 10:05
  • \$\begingroup\$ lookup.invert.tap { |h| h.each_key { |k| h[k] = have[h[k]] } } is another way. \$\endgroup\$ May 3, 2014 at 5:26
  • \$\begingroup\$ @CarySwoveland So essentially that's a more complicated way of doing the original second example, or is there some added benefit of doing it your way? \$\endgroup\$
    – Ressu
    May 3, 2014 at 17:46
  • \$\begingroup\$ Ressu, there is no benefit, and I didn't claim one, though I don't think it's any more complicated than the other ways. If only solutions that were claimed to be superior were posted, we would all be poorer for it. Maybe someone who has read my comment will think of using invert to advantage (in another time and place), or is unfamiliar with tap, looks it up and discovers that it's very handy indeed. \$\endgroup\$ May 3, 2014 at 17:59
  • 1
    \$\begingroup\$ I wasn't offended in the least. \$\endgroup\$ May 5, 2014 at 7:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.