3
\$\begingroup\$

I have a method that parses XML into an array of hashes.

Here is the original XML:

<rowset name="skillqueue" key="queuePosition" columns="queuePosition,typeID,level,startSP,endSP,startTime,endTime">
  <row queuePosition="0" typeID="20495" level="3" startSP="2829" endSP="16000" startTime="2013-03-04 16:25:50" endTime="2013-03-05 01:02:20"/>
  <row queuePosition="1" typeID="19767" level="4" startSP="40000" endSP="226275" startTime="2013-03-05 01:02:20" endTime="2013-03-07 06:40:31"/>
</rowset>

Here is the final array of hashes:

[{:queuePosition=>"0", :typeID=>"20495", :level=>"3", :startSP=>"2829", :endSP=>"16000", :startTime=>"2013-03-04 16:25:50", :endTime=>"2013-03-05 01:02:20"}, {:queuePosition=>"1", :typeID=>"19767", :level=>"4", :startSP=>"40000", :endSP=>"226275", :startTime=>"2013-03-05 01:02:20", :endTime=>"2013-03-07 06:40:31"}]

Here is the method, which uses Nokogiri for parsing:

def get_training_queue(xml_data)
  queue = []
  xml_data.xpath("//row").each do |skill_in_queue|
    skill = {}
    skill_in_queue.attributes.each do |details|
      skill[details[0].to_s.to_sym] = details[1].to_s
    end
    queue << skill
  end
  queue
end

This works great, but it feels a bit inelegant.

  • I'm curious if there is a better way to create the hashes within the internal loop?
  • I am calling to_s because I couldn't figure out a way to pull out just the key/value without doing so, and it 'feels' like I'm missing some more elegant way of doing that.
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

Start by using map instead of each plus <<; creating an array and then adding to it isn't very Ruby-like.

Second, Node#attributes is a hash already, so one way to go is to modify a duplicate of it, rather than "manually" copying each key/value to a new hash. Also, the keys are strings so to_s.to_sym can be replaced by just to_sym.

To do the hash-conversion, I've used the same keys.each approach as what Rails uses in its symbolize_keys! method.

def get_training_queue(xml_data)
  xml_data.xpath("//row").map do |row|
    skill = row.attributes.dup
    skill.keys.each do |key|
      skill[key.to_sym] = skill.delete(key).to_s
    end
    skill
  end
end

Here's a different, super-brief, approach that uses the Hash[ [key, value] , ... ] syntax

def get_training_queue(xml_data)
  xml_data.xpath("//row").map do |row|
    Hash[ row.attributes.map { |k, v| [k.to_sym, v.to_s] } ]
  end
end

Either one should give you the right result, though.

\$\endgroup\$
2
  • \$\begingroup\$ you can safely remove that to_a :-) \$\endgroup\$
    – tokland
    Commented Mar 5, 2013 at 23:54
  • \$\begingroup\$ @tokland Good call. Wonder why I even put that in there... :P \$\endgroup\$
    – Flambino
    Commented Mar 6, 2013 at 0:03
4
\$\begingroup\$

Notes:

  • Use functional Enumerable#map instead of imperative pattern obj = [] + Enumerable#each + Array#<<.
  • Use Kernel#Hash to build a hash from its pairs. Note that this method is very ugly and some prefer a more OOP approach, in that case check Enumerable#mash from Facets.
  • Use nokogiri_node.text.
  • Arguments in blocks can be unpacked.

I'd write:

require 'nokogiri'
require 'facets'

def get_training_queue(xml_data)
  xml_data.xpath("//row").map do |skill_in_queue|
    skill_in_queue.attributes.mash do |name, attribute|
      [name.to_sym, attribute.text]
    end
  end
end

All those eachs and in-place updates show that you think in imperative terms instead of functional, check this wiki page.

\$\endgroup\$
2
  • \$\begingroup\$ I had failed to get around to messing with facets up to this point - so interesting solution. Both answers presented in this thread were helpful, but Flambino offered two helpful solutions so I'm going to go ahead and issue them the answer. Upvote though :) \$\endgroup\$
    – Ecnalyr
    Commented Mar 5, 2013 at 15:44
  • \$\begingroup\$ @Ecnalyr: no problem, the solutions are almost identical. Make sure you read a bit about FP though, great stuff! \$\endgroup\$
    – tokland
    Commented Mar 6, 2013 at 9:30

Your Answer

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

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