0
votes

I have some hashes I want to iterate, and some have nested hashes which can go 3-4 levels deep, I am using an if statement at the moment to check to see if the value is a hash and then iterate through it again however I'm just repeating code here.

Is there a DRY way to do this using a method?

Also I want the final output to end up in a table, what's the best way to do this? That's why I had the multiple if statements originally so I could add the separate tags.

example method:

<% def hashTest(key, value) %>
    <% if value.is_a?(Hash) %>
        <%= key %>
            <% value.each do |key, value| %>
                <%= key %>
                <%= value %>
            <% end %>
    <% else %>
        <%= key %>
        <%= value %>
    <% end %>
<% end %>

and this is the mess of if statements I have...

<% parsed.each do |key, value| %>
    <% if value.is_a?(Hash) %>
        <%= key %>
        <br/>
        <% value.each do |key, value| %>
                <% if value.is_a?(Hash) %>
                    <%= key %>
                    <br/>
                    <% value.each do |key, value| %>
                            <% if value.is_a?(Hash) %>
                            <%= key %>
                            <br/>
                                <% value.each do |key, value| %>
                                    <%= key %>
                                    <%= value %>
                                    <br/>
                                <% end %>
                            <% else %>
                                <%= key %>
                                <%= value %>
                                <br/>
                            <% end %>
                    <% end %>
                <% else %>
                    <%= key %>
                    <%= value %>
                    <br/>
                <% end %>
        <% end %>
    <% else %>
        <%= key %>
        <%= value %>
        <br/>
    <% end %>
<% end %>

example input:

{
  "statement": {
    "generated": "2015-01-11",
    "due": "2015-01-25",
    "period": {
      "from": "2015-01-26",
      "to": "2015-02-25"
    }
  },
  "total": 136.03,
  "package": {
    "subscriptions": [
      { "type": "tv", "name": "Movies", "cost": 50.00 },
      { "type": "Phone", "name": "Landline", "cost": 5.00 },
      { "type": "broadband", "name": "Fibre", "cost": 16.40 }
    ],
    "total": 71.40
  },
  "callCharges": {
    "calls": [
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "07716393769", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 },
      { "called": "02074351359", "duration": "00:23:03", "cost": 2.13 }
    ],
    "total": 59.64
  },
  "Store": {
    "rentals": [
      { "title": "50 Shades of Grey", "cost": 4.99 }
    ],
    "purchases": [
      { "title": "That's what she said", "cost": 9.99 },
      { "title": "Broke back mountain", "cost": 9.99 }
    ],
    "total": 24.97
  }
}

Example output:

https://jsfiddle.net/un5ex8y1/1/

1
Please edit in your expected output for some sample input. We could deduce it from your mess of if statements, but we'd prefer not to have to do so. Also, not directly related to your question, but you are abusing ERb. You should not have this much code in your templates, ever. Especially not method definitions. The whole purpose of templates is to separate code from markup (see MVC and separation of concerns). - Amadan
Well that's why I need the method, so I can put it in the controller and do away with the mess. - Longshanks
I understand the mess in the second snippet; that is what you want to do away with. It is the first snippet that I object to, which is clearly being in a view, not in a controller. - Amadan
Ah, fair comment, that was just because I copied the if statement from the view just to put on here quickly, Added the example data. - Longshanks
So you just want keys and values of everything, separated by breaks? (You didn't give expected output. You don't need to make everything, but the first couple of lines would have been great.) - Amadan

1 Answers

1
votes

Assuming you want to display each key, each value, and each array element in a separate row,

def flatten_breakify(val)
  case val
  when Hash
    val.map { |k, v| "#{k}<br>#{flatten_breakify(v)}" }.join('<br>')
  when Array
    val.map(&method(:flatten_breakify)).join('<br>')
  else
    val
  end
end

Another, more elegant approach:

def flatten_all(val)
  case val
  when Hash
    val.flat_map { |k, v| [k, *flatten_all(v)] }
  when Array
    val.flat_map(&method(:flatten_all))
  else
    val
  end
end

then you can put in your template

<%= flatten_all(data).join('<br>') %>

in order not to mix HTML into controllers.

EDIT: I see you have added indent in your output, which has not been mentioned elsewhere. I'd recommend you use <ul> instead of plain <br> then, to achieve proper nesting, and solve the indent using CSS. You will not be able to use the second approach, since it has more structure now. The first piece of code, since it involves HTML, should not be in a controller, but in a helper (that's what helpers are for, for generating HTML from data).