Ruby Array Search Method Not Working As Intended - arrays

I am having trouble making the search method that i wrote in my script to work. Here is the relevant code from the script:
$records = []
def xml_extractor(document)
document.xpath("//record").each do |record|
$records << {"id" => record.xpath('id').text,
"first_name" => record.xpath('first_name').text,
"last_name" => record.xpath('last_name').text,
"email" => record.xpath('email').text,
"gender" => record.xpath('gender').text,
"ip_address" => record.xpath('ip_address').text,
"send_date" => record.xpath('send_date').text,
"email_body" => record.xpath('email_body').text,
"email_title" => record.xpath('email_title').text}
puts record
end
puts "\nAll records loaded!"
end
def search_by_ip(ip)
record_to_return = $records.select {|k| k["ip_address"] == ip.to_s}
puts JSON.pretty_generate(record_to_return)
end
Basically my xml_extractor method works fine and it stores everything into the array using nokogiri. The xml file that is being implemented has a thousand records each having its own first_name, last_name etc. But the problem is when i try to implement the search_by_ip method on the array a "null" value is returned, when what the method should really be doing is returning the entire record that belongs to that specific ip address. Also, i realised that every time i implement the xml_extractor method, i.e. when an xml document is parsed in into the array, the contents arent really saved in rather they are only displayed for while the loop is going. Which might be why I get a "null" for my search methods. Let me know what you guys think though.

I wrote an example of how to use OO to obtain what you want.
I don't have your document so I simplified your document to a 2 dimensional array
In the method read switch the comment to work with your xml
Each method can be chained and does only what is required
They can all be tested separatly (here by p'ting them)
class Xml_extractor
attr_reader :document, :records
def initialize document
#document = document
#records = []
end
def read
# #document.xpath("//record").each do |record|
#document.each do |record|
#records << {id: record[0], ip_address: record[1]}
end
self # return self so that you can chain another method
end
def search_by_ip(ip)
#return first of an array if found, nil otherwise
# attention to use a hash key here to search, not a string
#records.select {|k| k[:ip_address] == ip.to_s}.first
end
end
document = [[0, "192.168.0.1"], [1, "192.168.0.2"]]
p Xml_extractor.new(document).read.records
# [{:id=>0, :ip_address=>"192.168.0.1"}, {:id=>1, :ip_address=>"192.168.0.2"}]
p Xml_extractor.new(document).read.search_by_ip("192.168.0.2")
# [{:id=>1, :ip_address=>"192.168.0.2"}]
p Xml_extractor.new(document).read.search_by_ip("192.168.0.2").to_json
# "[{\"id\":1,\"ip_address\":\"192.168.0.2\"}]"

In ruby your method will return the last line. If you want your method to return data, you need to return it on the last line. puts doesn't return anything.
Try to change like this:
def search_by_ip(ip)
record_to_return = $records.select {|k| k["ip_address"] == ip.to_s}
puts JSON.pretty_generate(record_to_return)
record_to_return
end

Related

Ruby how to print/puts read file contents instead of a bunch of numbers/symbols

In ruby I have created a class and array, to read the contents from a text file then output them.
class Album
attr_accessor :artist, :title, :genre, :tracks
end
album = Album.new(album_title, album_artist, album_genre, tracks)
-> tracks is an array of multiple lines read from the text file using a while loop. Context below, a_file/music_file File.new("album.txt", "r")
class Track
attr_accessor :name, :location
def read_track(a_file)
track_title = a_file.gets()
track_location = a_file.gets()
track = Track.new(track_title, track_location)
end
def read_tracks(music_file)
tracks = Array.new()
count = music_file.gets().to_i()
track = music_file
index = 0
while (index < count)
track = read_track(music_file)
tracks << track
index += 1
end
return tracks
end
end
after album = Album.new(album_title, album_artist, album_genre, tracks), I passed the album to a different procedure print_tracks(album), and in print_tracks(album), I have puts album.tracks.
But instead of printing out several lines of track names and track locations, I get something that looks like this:
#<Track:0x000055c028027b08>
#<Track:0x000055c0280277c0>
#<Track:0x000055c028027630>
How do I print out the actual words on the file?
What you are getting in return are instances of your Track class. Each of those instances has access to attributes like name and location as specified under the class definition in attr_accessor. You can change your last statement (return tracks) (take note that return here is not needed since its the last statement in the method, the last thing will be returned by default in ruby).
Try this instead of return tracks
tracks.map{ |track| {track_name: track.name, track_location: track.location} }
This was you will end up with array of hashes with keys of track_name and track_location each containing a value of one track. I am not sure what kind of format you want to return, but this is a rather simple, yet flexible. The most simplistic way would be array of array, which you can get using:
racks.map{ |track| [track.name, track.location] }
You're observing the default behavior defined in Object#to_s and Object#inspect.
Ruby uses the to_s method to convert objects to strings and the inspect method to obtain string representations of objects suitable for debugging. By default, to_s and inspect are more or less the same thing. The only difference is inspect will also show the instance variables.
You can and should override these methods in your Track class. For example:
class Track
def to_s
"#{self.class.name} #{name} # {location}"
end
def inspect
"#<#{to_s}>"
end
end
track.to_s
puts track
# Track: name # /music/name.flac
track.inspect
p track
# #<Track: name # /music/name.flac>

How do I look up a value in a hash based on an array of keys?

With Ruby (2.4), I want to extend the core hash functionality to search for keys based on an array and return the value from the first element that can be found from that array. I have this in my lib/core_ext/hash_with_indifferent_access.rb file ...
class CaseInsensitiveHash < HashWithIndifferentAccess
# This method shouldn't need an override, but my tests say otherwise.
def [](key)
if key.kind_of?(Array)
find_by_arr(arr)
else
super convert_key(key)
end
end
protected
def convert_key(key)
key.respond_to?(:downcase) ? key.downcase : key
end
def find_by_arr(arr)
arr.inject(self){|h, k| h.to_h[k]}
end
end
However, it is not working as expected. In my code below, the search 'h[["a", "b"]]' should produce "1", because the first element, "a", is a key in my hash.
2.4.0 :001 > h = {"a" => 1, "b" => 2}
=> {"a"=>1, "b"=>2}
2.4.0 :002 > h["a"]
=> 1
2.4.0 :003 > h[["a", "b"]]
=> nil
How do I modify my code so that I can pass in an array as a key to a hash and it will start searching for values iteratively by each element in the array?
You've almost got it, but the problem is that h = { ... } creates a plain-old Hash, not the kind you've added these methods to.
The first fix is to do this:
h = CaseInsensitiveHash["a" => 1, "b" => 2]
Then you get the right type of object and your method actually runs.
There's a typo in your [] method which can be corrected:
def [](key)
case key
when Array
find_by_arr(key)
else
super convert_key(key)
end
end
Your reference to arr doesn't work because that's not defined.
The find_by_arr method also returns the last match, not the first. This can be fixed:
def find_by_arr(arr)
self[arr.first { |key| self[key] }]
end

How to iterate and scrape data in Ruby?

I am pretty new to programming and need some help/feedback on my code.
My goal is to scrape my data, which is working fine, and then display that data to my user in a numbered list. I am simply having difficulty displaying this data. I do not get any errors back my program simply skips my method altogether. Thanks in advance for any help/feedback!
class BestPlaces::Places
attr_accessor :name, :population, :places
##places = []
def self.list_places
# puts "this is inside list places"
self.scrape_places
end
def self.scrape_places
doc = Nokogiri::HTML(open("https://nomadlist.com/best-cities-to-live"))
places = doc.search("div.text h2.itemName").text
rank = doc.search("div.rank").text
places.collect{|e| e.text.strip}
puts "you are now in title"
##places << self.scrape_places
puts "#{rank}. #{places}"
end
end
end
CLI Page:
class BestPlaces::CLI
def list_places
puts "Welcome to the best places on Earth!"
puts #places = BestPlaces::Places.list_places
end
def call
list_places
menu
goodbye
end
end
There are a few things that could be addressed in this code, but let's first see a reworking:
require 'nokogiri'
require 'open-uri'
module BestPlaces
class Places
attr_accessor :name, :population, :places
def initialize
#places = []
end
def scrape_places
doc = Nokogiri::HTML(open("https://nomadlist.com/best-cities-to-live"))
places = doc.search("div.text h2.itemName")
ranks = doc.search("div.rank")
places.each{|e| #places << e.text.strip}
puts "you are now in title"
#places.each do |place|
i = #places.index(place)
puts "#{ranks[i].text}. #{place}"
end
end
end
class CLI
def list_places
puts "Welcome to the best places on Earth!"
BestPlaces::Places.scrape_places
end
def call
list_places
menu
goodbye
end
end
end
You have what looks to be an incomplete Module/Class setup. One can call the above like so:
bp = BestPlaces::Places.new
bp.scrape_places
The ##places variable was unnecessary, we can use #places instead to hold values that need to be accessed within the Places class. Also, nokogiri returns a string object when using the .text method on search results, which means you cannot iterate over them like an array. I hope this helps.

How to subclass an Array in Ruby?

I'm trying to subclass Array to implement a map method that returns instances of my Record class. I'm trying to create a sort of "lazy" array that only instantiates objects as they are needed to try and avoid allocating too many Ruby objects at once. I'm hoping to make better use of the garbage collector by only instantiating an object on each iteration.
class LazyArray < Array
def initialize(results)
#results = results
end
def map(&block)
record = Record.new(#results[i]) # how to get each item from #results for each iteration?
# how do I pass the record instance to the block for each iteration?
end
end
simple_array = [{name: 'foo'}, {name: 'bar'}]
lazy_array_instance = LazyArray.new(simple_array)
expect(lazy_array_instance).to be_an Array
expect(lazy_array_instance).to respond_to :map
lazy_array_instance.map do |record|
expect(record).to be_a Record
end
How can I subclass Array so that I can return an instance of my Record class in each iteration?
From what I know, you shouldn't have to do anything like this at all. Using .lazy you can perform lazy evaluation of arrays:
simple_array_of_results.lazy.map do |record|
# do something with Record instance
end
Now, you've got some odd situation where you're doing something like -
SomeOperation(simple_array_of_results)
and either you want SomeOperation to do it's thing lazily, or you want the output to be something lazy -
lazily_transformed_array_of_results = SomeOperation(simple_array_of_results)
page_of_results = lazily_transformed_array_of_results.take(10)
If that sounds right... I'd expect it to be as simple as:
SomeOperation(simple_array_of_results.lazy)
Does that work? array.lazy returns an object that responds to map, after all...
Edit:
...after reading your question again, it seems like what you actually want is something like:
SomeOperation(simple_array_of_results.lazy.collect{|r| SomeTransform(r)})
SomeTransform is whatever you're thinking of that takes that initial data and uses it to create your objects ("as needed" becoming "one at a time"). SomeOperation is whatever it is that needs to be passed something that responds to map.
So you have an array of simple attributes or some such and you want to instantiate an object before calling the map block. Sort of pre-processing on a value-by-value basis.
class Record
attr_accessor :name
def initialize(params={})
self.name = params[:name]
end
end
require 'delegate'
class MapEnhanced < SimpleDelegator
def map(&block)
#delegate_ds_obj.map do |attributes|
object = Record.new(attributes)
block.call(object)
end
end
end
array = MapEnhanced.new([{name: 'Joe'}, {name: 'Pete'}])
array.map {|record| record.name }
=> ["Joe" "Pete"]
An alternative (which will allow you to keep object.is_a? Array)
class MapEnhanced < Array
alias_method :old_map, :map
def map(&block)
old_map do |attributes|
object = Record.new(attributes)
block.call(object)
end
end
end

Find key/value pairs deep inside a hash containing an arbitrary number of nested hashes and arrays

A web service is returning a hash that contains an unknown number of nested hashes, some of which contain an array, which in turn contains an unknown number of nested hashes.
Some of the keys are not unique -- i.e. are present in more than one of the nested hashes.
However, all the keys that I actually care about are all unique.
Is there someway I can give a key to the top-level hash, and get back it's value even if the key-value pair is buried deep in this morass?
(The web service is Amazon Product Advertising API, which slightly varies the structure of the results that it gives depending on the number of results and the search types permitted in each product category.)
Here's a simple recursive solution:
def nested_hash_value(obj,key)
if obj.respond_to?(:key?) && obj.key?(key)
obj[key]
elsif obj.respond_to?(:each)
r = nil
obj.find{ |*a| r=nested_hash_value(a.last,key) }
r
end
end
h = { foo:[1,2,[3,4],{a:{bar:42}}] }
p nested_hash_value(h,:bar)
#=> 42
No need for monkey patching, just use Hashie gem: https://github.com/intridea/hashie#deepfind
user = {
name: { first: 'Bob', last: 'Boberts' },
groups: [
{ name: 'Rubyists' },
{ name: 'Open source enthusiasts' }
]
}
user.extend Hashie::Extensions::DeepFind
user.deep_find(:name) #=> { first: 'Bob', last: 'Boberts' }
For arbitrary Enumerable objects, there is another extension available, DeepLocate: https://github.com/intridea/hashie#deeplocate
Combining a few of the answers and comments above:
class Hash
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.is_a? Enumerable
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
Ruby 2.3 introduces Hash#dig, which allows you to do:
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
A variation of barelyknown's solution: This will find all the values for a key in a hash rather than the first match.
class Hash
def deep_find(key, object=self, found=[])
if object.respond_to?(:key?) && object.key?(key)
found << object[key]
end
if object.is_a? Enumerable
found << object.collect { |*a| deep_find(key, a.last) }
end
found.flatten.compact
end
end
{a: [{b: 1}, {b: 2}]}.deep_find(:b) will return [1, 2]
Despite this appearing to be a common problem, I've just spent a while trying to find/come up with exactly what I need, which I think is the same as your requirement. Neither of the links in the first response are spot-on.
class Hash
def deep_find(key)
key?(key) ? self[key] : self.values.inject(nil) {|memo, v| memo ||= v.deep_find(key) if v.respond_to?(:deep_find) }
end
end
So given:
hash = {:get_transaction_list_response => { :get_transaction_list_return => { :transaction => [ { ...
The following:
hash.deep_find(:transaction)
will find the array associated with the :transaction key.
This is not optimal as the inject will continue to iterate even if memo is populated.
I use the following code
def search_hash(hash, key)
return hash[key] if hash.assoc(key)
hash.delete_if{|key, value| value.class != Hash}
new_hash = Hash.new
hash.each_value {|values| new_hash.merge!(values)}
unless new_hash.empty?
search_hash(new_hash, key)
end
end
I ended up using this for a small trie search I wrote:
def trie_search(str, obj=self)
if str.length <= 1
obj[str]
else
str_array = str.chars
next_trie = obj[str_array.shift]
next_trie ? trie_search(str_array.join, next_trie) : nil
end
end
Note: this is just for nested hashes at the moment. Currently no array support.
Because Rails 5 ActionController::Parameters no longer inherits from Hash, I've had to modify the method and make it specific to parameters.
module ActionController
class Parameters
def deep_find(key, object=self, found=nil)
if object.respond_to?(:key?) && object.key?(key)
return object[key]
elsif object.respond_to?(:each)
object = object.to_unsafe_h if object.is_a?(ActionController::Parameters)
object.find { |*a| found = deep_find(key, a.last) }
return found
end
end
end
end
If the key is found, it returns the value of that key, but it doesn't return an ActionController::Parameter object so Strong Parameters are not preserved.

Resources