Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I convert XML into a hash in Rails?

How do I convert an XML body to a hash in Ruby?

I have an XML body which I'd like to parse into a hash

<soap:Body>
    <TimesInMyDAY>
        <TIME_DATA>
            <StartTime>2010-11-10T09:00:00</StartTime>
            <EndTime>2010-11-10T09:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:20:00</StartTime>
            <EndTime>2010-11-10T09:40:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:40:00</StartTime>
            <EndTime>2010-11-10T10:00:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:00:00</StartTime>
            <EndTime>2010-11-10T10:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:40:00</StartTime>
            <EndTime>2010-11-10T11:00:00</EndTime>
        </TIME_DATA>
    </TimesInMyDAY>
</soap:Body>

I'd like to convert it into a hash like this:

{ :times_in_my_day => { 
    :time_data = > [
        {:start_time=>"2010-11-10T09:00:00", :end_time => "2010-11-10T09:20:00" },
        {:start_time=>"2010-11-10T09:20:00", :end_time => "2010-11-10T09:40:00" },
        {:start_time=>"2010-11-10T09:40:00", :end_time => "2010-11-10T10:00:00" },
        {:start_time=>"2010-11-10T10:00:00", :end_time => "2010-11-10T10:20:00" },
        {:start_time=>"2010-11-10T10:40:00", :end_time => "2010-11-10T11:00:00" }
        ]
    } 
}

Ideally, the tags would convert to snake_case symbols and become keys within the hash.

Also, the datetimes are missing their time zone offsets. They are in the local time zone (not UTC). So I'd like to parse it to show the local offset and then convert the xml datetime strings into Rails DateTime objects. The resulting array would be something like:

{ :times_in_my_day => { 
    :time_data = > [
        {:start_time=>Wed Nov 10 09:00:00 -0800 2010, :end_time => Wed Nov 10 9:20:00 -0800 2010 },
        {:start_time=>Wed Nov 10 09:20:00 -0800 2010, :end_time => Wed Nov 10 9:40:00 -0800 2010 },
        {:start_time=>Wed Nov 10 09:40:00 -0800 2010, :end_time => Wed Nov 10 10:00:00 -0800 2010 },
        {:start_time=>Wed Nov 10 10:00:00 -0800 2010, :end_time => Wed Nov 10 10:20:00 -0800 2010 },
        {:start_time=>Wed Nov 10 10:40:00 -0800 2010, :end_time => Wed Nov 10 11:00:00 -0800 2010 }
        ]
    } 
}

I was able to convert a single datetime with the parse and in_time_zone methods this way:

Time.parse(xml_datetime).in_time_zone(current_user.time_zone)

But I'm not quite sure the best way to parse the times while converting the XML into a hash.

I'd appreciate any advice. Thanks!

Edit

The code for converting the datetime string into a Rails DateTime object is wrong. That will parse the xml datetime string to the system's timezone offset and then convert that time to the user's timezone. The correct code is:

Time.zone.parse(xml_datetime)

If the user has a different time zone other than the system, this will add the user's time zone offset to the original datetime string. There's a Railscast on how to enable user timezone preferences here: http://railscasts.com/episodes/106-time-zones-in-rails-2-1.

like image 521
Chanpory Avatar asked Nov 11 '10 02:11

Chanpory


2 Answers

Hash.from_xml(xml) is simple way to solve this. Its activesupport method

like image 94
Taimoor Changaiz Avatar answered Oct 13 '22 23:10

Taimoor Changaiz


I used to use XML::Simple in Perl because parsing XML using Perl was a PITA.

When I switched to Ruby I ended up using Nokogiri, and found it to be very easy to use for parsing HTML and XML. It's so easy that I think in terms of CSS or XPath selectors and don't miss a XML-to-hash converter.

require 'ap'
require 'date'
require 'time'
require 'nokogiri'

xml = %{
<soap:Body>
    <TimesInMyDAY>
        <TIME_DATA>
            <StartTime>2010-11-10T09:00:00</StartTime>
            <EndTime>2010-11-10T09:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:20:00</StartTime>
            <EndTime>2010-11-10T09:40:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T09:40:00</StartTime>
            <EndTime>2010-11-10T10:00:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:00:00</StartTime>
            <EndTime>2010-11-10T10:20:00</EndTime>
        </TIME_DATA>
        <TIME_DATA>
            <StartTime>2010-11-10T10:40:00</StartTime>
            <EndTime>2010-11-10T11:00:00</EndTime>
        </TIME_DATA>
    </TimesInMyDAY>
</soap:Body>
}

time_data = []

doc = Nokogiri::XML(xml)
doc.search('//TIME_DATA').each do |t|
  start_time = t.at('StartTime').inner_text
  end_time = t.at('EndTime').inner_text
  time_data << {
    :start_time => DateTime.parse(start_time),
    :end_time   => Time.parse(end_time)
  }
end

puts time_data.first[:start_time].class
puts time_data.first[:end_time].class
ap time_data[0, 2]

with the output looking like:

DateTime
Time
[
    [0] {
        :start_time => #<DateTime: 2010-11-10T09:00:00+00:00 (19644087/8,0/1,2299161)>,
          :end_time => 2010-11-10 09:20:00 -0700
    },
    [1] {
        :start_time => #<DateTime: 2010-11-10T09:20:00+00:00 (22099598/9,0/1,2299161)>,
          :end_time => 2010-11-10 09:40:00 -0700
    }
]

The time values are deliberately parsed into DateTime and Time objects to show that either could be used.

like image 34
the Tin Man Avatar answered Oct 13 '22 23:10

the Tin Man