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.
2 Answers 2
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.
-
\$\begingroup\$ you can safely remove that
to_a
:-) \$\endgroup\$tokland– tokland2013年03月05日 23:54:22 +00:00Commented Mar 5, 2013 at 23:54 -
\$\begingroup\$ @tokland Good call. Wonder why I even put that in there... :P \$\endgroup\$Flambino– Flambino2013年03月06日 00:03:12 +00:00Commented Mar 6, 2013 at 0:03
Notes:
- Use functional
Enumerable#map
instead of imperative patternobj = []
+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 each
s and in-place updates show that you think in imperative terms instead of functional, check this wiki page.
-
\$\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– Ecnalyr2013年03月05日 15:44:09 +00:00Commented 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– tokland2013年03月06日 09:30:38 +00:00Commented Mar 6, 2013 at 9:30