How would I construct a Hash from this scenario in Ruby? - ruby

Given I have the following code:
ENDPOINT = 'http://api.eventful.com'
API_KEY = 'PbFVZfjTXJQWrnJp'
def get_xml(url, options={})
compiled_url = "#{ENDPOINT}/rest#{url}" << "?app_key=#{API_KEY}&sort_order=popularity"
options.each { |k, v| compiled_url << "&#{k.to_s}=#{v.to_s}" }
REXML::Document.new((Net::HTTP.get(URI.parse(URI.escape(compiled_url)))))
end
def event_search(location, date)
get_xml('/events/search',
:location => "#{location}, United Kingdom",
:date => date
)
end
And we access the XML data formatted by REXML::Document like this:
events = event_search('London', 'Today').elements
And we can access these elements like this (this prints all the titles in the events):
events.each('search/events/event/title') do |title|
puts title.text
end
The XML I'm using can be found here. I would like this construct a Hash like so:
{"Title1" => {:title => 'Title1', :date => 'Date1', :post_code => 'PostCode1'},
"Title2" => {:title => 'Title2', :date => 'Date2', :post_code => 'PostCode2'}}
When using events.each('search/events/event/title'), events.each('search/events/event/date'), and events.each('search/events/event/post_code').
So I want to create a Hash from the XML provided by the URL I have included above. Thanks!

You should loop over the events themselves, not the titles. Something like this
events_by_title = {}
elements.each('search/events/event') do |event|
title = event.get_elements('title').first.text
events_by_title[title] = {
:title => title,
:date => event.get_elements('start_time').first.text
:post_code => event.get_elements('postal_code').first.text,
}
end

Get the root element using root() on the REXML:Document object then use each_element("search/events/event") to iterate over "event" node. You can then extract the different values out of it using the different methods on element: http://ruby-doc.org/stdlib-1.9.3/libdoc/rexml/rdoc/REXML/Element.html

Related

How to generate direct access keys to nested hash which contains hash and arrays as values?

I want to compare two XML files where one is input and the other is output. I am converting both into a hash.
My idea is to get all the keys from the input XML converted to hash, and search each key in both the input and output hashes for their respective key/value pairs.
I have a hash:
{
"requisition_header" => {
"requested_by" => {"login" => "coupasupport"},
"department" => {"name" => "Marketing"},
"ship_to_address" => {"name" => "Address_1431693296"},
"justification" => nil,
"attachments" => [],
"requisition_lines" => [
{
"description" => "Cleaning Services for Building A",
"line_num" => 1,
"need_by_date" => 2010-09-23 07:00:00 UTC,
"source_part_num" => nil,
"supp_aux_part_num" => nil,
"unit_price" => #<BigDecimal:a60520c,'0.3E4',9(18)>,
"supplier" => {"name" => "amazon.com"},
"account" => {
"code" => "SF-Marketing-Indirect",
"account_type" => {"name" => "Ace Corporate"}
},
"currency" => {"code" => "USD"},
"payment_term" => {"code" => "Net 30"},
"shipping_term" => {"code" => "Standard"},
"commodity" => {"name" => "Marketing-Services"}
}
]
}
}
It is nested and all the values are not directly accessible.
I want a way to generate direct access to each value in the hash.
For example:
requisition_header.requested_by.login
will access "coupasupport".
requisition_header.department.name
will access "Marketing".
requisition_header.requisition_lines[0].description
will access "Cleaning Services for Building A".
requisition_header.requisition_lines[0].line_num
will access "1".
requisition_header.requisition_lines[0].need_by_date
will access "2010-09-23 07:00:00 UTC".
Each key built can be used to search for the value directly inside the hash.
That could be done with the following method, that translates the nested hash into nested OpenStructs:
require 'ostruct'
def deep_structify(hash)
result = {}
hash.each do |key, value|
result[key] = value.is_a?(Hash) ? deep_structify(value) : value
end if hash
OpenStruct.new(result)
end
hash = {"requisition_header"=>{"requested_by"=>{"login"=>"coupasupport"}, "department"=>{"name"=>"Marketing"}, "ship_to_address"=>{"name"=>"Address_1431693296"}, "justification"=>nil, "attachments"=>[], "requisition_lines"=>[{"description"=>"Cleaning Services for Building A", "line_num"=>1, "need_by_date"=>2010-09-23 07:00:00 UTC, "source_part_num"=>nil, "supp_aux_part_num"=>nil, "unit_price"=>#<BigDecimal:a60520c,'0.3E4',9(18)>, "supplier"=>{"name"=>"amazon.com"}, "account"=>{"code"=>"SF-Marketing-Indirect", "account_type"=>{"name"=>"Ace Corporate"}}, "currency"=>{"code"=>"USD"}, "payment_term"=>{"code"=>"Net 30"}, "shipping_term"=>{"code"=>"Standard"}, "commodity"=>{"name"=>"Marketing-Services"}}]}}
struct = deep_structify(hash)
struct.requisition_header.department.name
#=> "Marketing"
You can do it by overriding OpenStruct#new as well,
require 'ostruct'
class DeepStruct < OpenStruct
def initialize(hash=nil)
#table = {}
#hash_table = {}
if hash
hash.each do |k,v|
#table[k.to_sym] = (v.is_a?(Hash) ? self.class.new(v) : v)
#hash_table[k.to_sym] = v
new_ostruct_member(k)
end
end
end
def to_h
#hash_table
end
end
Now you can do:
require 'deep_struct'
hash = {"requisition_header"=>{"requested_by"=>{"login"=>"coupasupport"}, "department"=>{"name"=>"Marketing"}, "ship_to_address"=>{"name"=>"Address_1431693296"}, "justification"=>nil, "attachments"=>[], "requisition_lines"=>[{"description"=>"Cleaning Services for Building A", "line_num"=>1, "need_by_date"=>2010-09-23 07:00:00 UTC, "source_part_num"=>nil, "supp_aux_part_num"=>nil, "unit_price"=>#<BigDecimal:a60520c,'0.3E4',9(18)>, "supplier"=>{"name"=>"amazon.com"}, "account"=>{"code"=>"SF-Marketing-Indirect", "account_type"=>{"name"=>"Ace Corporate"}}, "currency"=>{"code"=>"USD"}, "payment_term"=>{"code"=>"Net 30"}, "shipping_term"=>{"code"=>"Standard"}, "commodity"=>{"name"=>"Marketing-Services"}}]}}
mystruct = DeepStruct.new hash
mystruct.requisition_header.requested_by.login # => coupasupport
mystruct.requisition_header.to_h # => {"requested_by"=>{"login"=>"coupasupport"}
You could use BasicObject#method_missing:
Code
class Hash
def method_missing(key,*args)
(args.empty? && key?(key)) ? self[key] : super
end
end
Example
hash = { animals: {
pets: { dog: "Diva", cat: "Boots", python: "Stretch" },
farm: { pig: "Porky", chicken: "Little", sheep: "Baa" }
},
finishes: {
tinted: { stain: "Millers", paint: "Oxford" },
clear: { lacquer: "Target", varnish: "Topcoat" }
}
}
hash.finishes.tinted.stain
#=> "Millers
hash.animals.pets.cat
#=> "Boots"
hash.animals.pets
#=> {:dog=>"Diva", :cat=>"Boots", :python=>"Stretch"}
hash.animals
#=> {:pets=>{:dog=>"Diva", :cat=>"Boots", :python=>"Stretch"},
# :farm=>{:pig=>"Porky", :chicken=>"Little", :sheep=>"Baa"}}
Reader challenge
There is a potential "gotcha" with this approach. I leave it to the reader to identify it. My example contains a clue. (Mind you, there may be other problems I haven't thought of.)

Iterating over an array to create a nested hash

I am trying to create a nested hash from an array that has several elements saved to it. I've tried experimenting with each_with_object, each_with_index, each and map.
class Person
attr_reader :name, :city, :state, :zip, :hobby
def initialize(name, hobby, city, state, zip)
#name = name
#hobby = hobby
#city = city
#state = state
#zip = zip
end
end
steve = Person.new("Steve", "basketball","Dallas", "Texas", 75444)
chris = Person.new("Chris", "piano","Phoenix", "Arizona", 75218)
larry = Person.new("Larry", "hunting","Austin", "Texas", 78735)
adam = Person.new("Adam", "swimming","Waco", "Texas", 76715)
people = [steve, chris, larry, adam]
people_array = people.map do |person|
person = person.name, person.hobby, person.city, person.state, person.zip
end
Now I just need to turn it into a hash. One issue I am having is, when I'm experimenting with other methods, I can turn it into a hash, but the array is still inside the hash. The expected output is just a nested hash with no arrays inside of it.
# Expected output ... create the following hash from the peeps array:
#
# people_hash = {
# "Steve" => {
# "hobby" => "golf",
# "address" => {
# "city" => "Dallas",
# "state" => "Texas",
# "zip" => 75444
# }
# # etc, etc
Any hints on making sure the hash is a nested hash with no arrays?
This works:
person_hash = Hash[peeps_array.map do |user|
[user[0], Hash['hobby', user[1], 'address', Hash['city', user[2], 'state', user[3], 'zip', user[4]]]]
end]
Basically just use the ruby Hash [] method to convert each of the sub-arrays into an hash
Why not just pass people?
people.each_with_object({}) do |instance, h|
h[instance.name] = { "hobby" => instance.hobby,
"address" => { "city" => instance.city,
"state" => instance.state,
"zip" => instance.zip } }
end

Using marshal_load with OpenStruct

How do I use OpenStruct's marshal_load utility? It doesn't appear to work as intended.
The docs give this example, but it doesn't appear to work.
require 'ostruct'
event = OpenStruct.new
hash = { 'time' => Time.now, 'title' => 'Birthday Party' }
event.marshal_load(hash)
event.title # => nil
If not this way, how do I load a hash into an OpenStruct (without using the constructor)?
For context: I'm loading a hash in from a YAML file and loading it into an existing instance of an OpenStruct subclass.
Try with a symbol based hash. That worked for me.
#works.rb
hash = { :time => Time.now, :title => 'Birthday Party' }
event.marshal_load(hash)
The marshal_load method exists to provide support for Marshal.load.
event = OpenStruct.new({ 'time' => Time.now, 'title' => 'Birthday Party' })
binary = Marshal.dump(event)
loaded = Marshal.load(binary) # the OpenStruct
The easiest way to programmatically load a hash into a struct is using send:
event = OpenStruct.new
hash.each do |key, value|
event.send("#{key}=", value)
end

Ruby: Loop through hash and check if a key exists to determine markup and data to be displayed

This is my first time working with Ruby, so I may be approaching this incorrectly.
I am trying to go through a hash to display it's contents. As I'm going through the hash I'll need to test if a key exists, like city. If city doesn't exist then it shouldn't display the address. This is where I've started with building my hash:
# app.rb
set :haml, :format => :html5
get "/" do
#users = Hash[
[["name", "bill"], ["city", "nyc"], ["address", "street"]],
[["name", "ted"], ["city", "denver"]],
[["name", "sam"], ["address", "road"]]
]
haml :index
end
And this is how I am looping through the hash:
# layout.haml
- #users.each do |user|
- user.each do |u|
- u.each do |b|
= b
Once I get to b it will display all of the content like so:
["name", "bill"]
["city", "nyc"]
["address", "street"]
["name", "ted"]
["city", "denver"]
In the loop, how can I display the name as well as check to see if the address exists for each user to determine if the city should be displayed as well as any markup that may need to be added? It would ideally display something like:
<p>bill, <span class="address">nyc, street</span></p>
<p>ted</p>
<p>sam, <span class="address">road</span></p>
Am I creating the Hash properly to do it this way?
Instead of what you are trying to do with nested arrays inside a hash, it would be better to have an array that contains user hashes:
#users = [
{ :name => 'bill', :city => 'city', :address => 'street' },
{ :name => 'ted', :city => 'denver' },
{ :name => 'sam', :address => 'road' }
]
With that, you can do something like this:
- #users.each do |user|
= user[:name]
- if user.has_key?(:address) && user.has_key?(:city)
= "#{user[:address]}, #{user[:city]}"
- elsif user.has_key?(:address)
= "#{user[:address]}"

Comparing lists of field-hashes with equivalent AR-objects

I have a list of hashes, as such:
incoming_links = [
{:title => 'blah1', :url => "http://blah.com/post/1"},
{:title => 'blah2', :url => "http://blah.com/post/2"},
{:title => 'blah3', :url => "http://blah.com/post/3"}]
And an ActiveRecord model which has fields in the database with some matching rows, say:
Link.all =>
[<Link#2 #title='blah2' #url='...post/2'>,
<Link#3 #title='blah3' #url='...post/3'>,
<Link#4 #title='blah4' #url='...post/4'>]
I'd like to do set operations on Link.all with incoming_links so that I can figure out that <Link#4 ...> is not in the set of incoming_links, and {:title => 'blah1', :url =>'http://blah.com/post/1'} is not in the Link.all set, like so:
#pseudocode
#incoming_links = as above
links = Link.all
expired_links = links - incoming_links
missing_links = incoming_links - links
expired_links.destroy
missing_links.each{|link| Link.create(link)}
Crappy solution a):
I'd rather not rewrite Array#- and such, and I'm okay with converting incoming_links to a set of unsaved Link objects; so I've tried overwriting hash eql? and so on in Link so that it ignored the id equality that AR::Base provides by default. But this is the only place this sort of equality should be considered in the application - in other places the Link#id default identity is required. Is there some way I could subclass Link and apply the hash, eql?, etc overwriting there?
Crappy solution b):
The other route I've tried is to pull out the attributes hash for each Link and doing a .slice('id',...etc) to prune the hashes down. But this requires writing seperate - methods for keeping track of the Link objects while doing set operations on the hashes, and writing seperate Proxy classes to wrap the incoming_links hashes and Links, which seems a bit overkill. Nonetheless, this is the current solution for me.
Can you think of a better way to design this interaction? Extra credit for cleanliness.
try this
incoming_links = [
{:title => 'blah1', :url => "http://blah.com/post/1"},
{:title => 'blah2', :url => "http://blah.com/post/2"},
{:title => 'blah3', :url => "http://blah.com/post/3"}]
ar_links = Link.all(:select => 'title, url').map(&:attributes)
# wich incoming links are not in ar_links
incoming_links - ar_links
# and vice versa
ar_links - incoming_links
upd
For your Link model:
def self.not_in_array(array)
keys = array.first.keys
all.reject do |item|
hash = {}
keys.each { |k| hash[k] = item.send(k) }
array.include? hash
end
end
def self.not_in_class(array)
keys = array.first.keys
class_array = []
all.each do |item|
hash = {}
keys.each { |k| hash[k] = item.send(k) }
class_array << hash
end
array - class_array
end
ar = [{:title => 'blah1', :url => 'http://blah.com/ddd'}]
Link.not_in_array ar
#=> all links from Link model which not in `ar`
Link.not_in_class ar
#=> all links from `ar` which not in your Link model
If you rewrite the equality method, will ActiveRecord complain still?
Can't you do something similar to this (as in a regular ruby class):
class Link
attr_reader :title, :url
def initialize(title, url)
#title = title
#url = url
end
def eql?(another_link)
self.title == another_link.title and self.url == another_link.url
end
def hash
title.hash * url.hash
end
end
aa = [Link.new('a', 'url1'), Link.new('b', 'url2')]
bb = [Link.new('a', 'url1'), Link.new('d', 'url4')]
(aa - bb).each{|x| puts x.title}
The requirements are:
# Keep track of original link objects when
# comparing against a set of incomplete `attributes` hashes.
# Don't alter the `hash` and `eql?` methods of Link permanently,
# or globally, throughout the application.
The current solution is in effect using Hash's eql? method, and annotating the hashes with the original objects:
class LinkComp < Hash
LINK_COLS = [:title, :url]
attr_accessor :link
def self.[](args)
if args.first.is_a?(Link) #not necessary for the algorithm,
#but nice for finding typos and logic errors
links = args.collect do |lnk|
lk = super(lnk.attributes.slice(*(LINK_COLS.collect(&:to_s)).to_a)
lk.link = lnk
lk
end
elsif args.blank?
[]
#else #raise error for finding typos
end
end
end
incoming_links = [
{:title => 'blah1', :url => "http://blah.com/post/1"},
{:title => 'blah2', :url => "http://blah.com/post/2"},
{:title => 'blah3', :url => "http://blah.com/post/3"}]
#Link.all =>
#[<Link#2 #title='blah2' #url='...post/2'>,
# <Link#3 #title='blah3' #url='...post/3'>,
# <Link#4 #title='blah4' #url='...post/4'>]
incoming_links= LinkComp[incoming_links.collect{|i| Link.new(i)}]
links = LinkComp[Link.all] #As per fl00r's suggestion
#this could be :select'd down somewhat, w.l.o.g.
missing_links = (incoming_links - links).collect(&:link)
expired_links = (links - incoming_links).collect(&:link)

Resources