How can I improve? - ruby

I was wondering if anyone could point out a cleaner better way to write my code which is pasted here. The code scrapes some data from yelp and processes it into a json format. The reason I'm not using hash.to_json is because it throws some sort of stack error which I can only assume is due to the hash being too large (It's not particularly large).
Response object = a hash
text = the output which saves to file
Anyways guidance appreciated.
def mineLocation
client = Yelp::Client.new
request = Yelp::Review::Request::GeoPoint.new(:latitude=>13.3125,:longitude => -6.2468,:yws_id => 'nicetry')
response = client.search(request)
response['businesses'].length.times do |businessEntry|
text =""
response['businesses'][businessEntry].each { |key, value|
if value.class == Array
value.length.times { |arrayEntry|
text+= "\"#{key}\":["
value[arrayEntry].each { |arrayKey,arrayValue|
text+= "{\"#{arrayKey}\":\"#{arrayValue}\"},"
}
text+="]"
}
else
text+="\"#{arrayKey}\":\"#{arrayValue}\","
end
}
end
end

It looks like all your code is ultimately doing is this:
require 'json'
def mine_location
client = Yelp::Client.new
request = Yelp::Review::Request::GeoPoint.new(latitude: 13.3125,
longitude: -6.2468, yws_id: 'nicetry')
response = client.search(request)
return response['businesses'].to_json
end
Which works just fine for me.
If, for whatever reason you really must write your own implementation of a JSON emitter, here's a couple of tips for you.
The number 1 thing you completely ignore in your code is that Ruby is an object-oriented language, more specifically a class-based object-oriented language. This means that problems are solved by constructing a network of objects that communicate with each other via message passing and respond to those messages by executing methods defined in classes to which those objects belong.
This gives us a lot of power: dynamic dispatch, polymorphism, encapsulation and a ton of others. Leveraging those, your JSON emitter would look something like this:
class Object
def to_json; to_s end
end
class NilClass
def to_json; 'null' end
end
class String
def to_json; %Q'"#{to_s}"' end
end
class Array
def to_json; "[#{map(&:to_json).join(', ')}]" end
end
class Hash
def to_json; "{#{map {|k, v| "#{k.to_json}: #{v.to_json}" }.join(', ')}}" end
end
mine_location looks just like above, except obviously without the require 'json' part.
If you want your JSON nicely formatted, you could try something like this:
class Object
def to_json(*) to_s end
end
class String
def to_json(*) inspect end
end
class Array
def to_json(indent=0)
"[\n#{' ' * indent+=1}#{
map {|el| el.to_json(indent) }.join(", \n#{' ' * indent}")
}\n#{' ' * indent-=1}]"
end
end
class Hash
def to_json(indent=0)
"{\n#{' ' * indent+=1}#{
map {|k, v|
"#{k.to_json(indent)}: #{v.to_json(indent)}"
}.join(", \n#{' ' * indent}")
}\n#{' ' * indent-=1}}"
end
end
There's actually nothing Ruby-specific in this code. This is pretty much exactly what the solution would look like in any other class-based object-oriented language like Java, for example. It's just object-oriented design 101.
The only thing which is language-specific is how to "modify" classes and add methods to them. In Ruby or Python, you literally just modify the class. In C# and Visual Basic.NET, you would probably use extension methods, in Scala you would use implicit conversions and in Java maybe the Decorator Design Pattern.
Another huge problem with your code is that you are trying to solve a problem which is obviously recursive without actually ever recursing. This just can't work. The code you wrote is basically Fortran-57 code: procedural with no objects and no recursion. Even just moving one step up from Fortran to, say, Pascal, gives you a nice recursive procedural solution:
def jsonify(o)
case o
when Hash
"{#{o.map {|k, v| "#{jsonify(k)}: #{jsonify(v)}" }.join(', ')}}"
when Array
"[#{o.map(&method(:jsonify)).join(', ')}]"
when String
o.inspect
when nil
'null'
else
o.to_s
end
end
Of course, you can play the same game with indentations here:
def jsonify(o, indent=0)
case o
when Hash
"{\n#{' ' * indent+=1}#{
o.map {|k, v|
"#{jsonify(k, indent)}: #{jsonify(v, indent)}"
}.join(", \n#{' ' * indent}") }\n#{' ' * indent-=1}}"
when Array
"[\n#{' ' * indent+=1}#{
o.map {|el| jsonify(el, indent) }.join(", \n#{' ' * indent}") }\n#{' ' * indent-=1}]"
when String
o.inspect
when nil
'null'
else
o.to_s
end
end
Here's the indented output of puts mine_location, produced using either the second (indented) version of to_json or the second version of jsonify, it doesn't really matter, they both have the same output:
[
{
"name": "Nickies",
"mobile_url": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ",
"city": "San Francisco",
"address1": "466 Haight St",
"zip": "94117",
"latitude": 37.772201,
"avg_rating": 4.0,
"address2": "",
"country_code": "US",
"country": "USA",
"address3": "",
"photo_url_small": "http://static.px.yelp.com/bpthumb/mPNTiQm5HVqLLcUi8XrDiA/ss",
"url": "http://yelp.com/biz/nickies-san-francisco",
"photo_url": "http://static.px.yelp.com/bpthumb/mPNTiQm5HVqLLcUi8XrDiA/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_4.png",
"is_closed": false,
"id": "yyqwqfgn1ZmbQYNbl7s5sQ",
"nearby_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA",
"state_code": "CA",
"reviews": [
{
"rating": 3,
"user_photo_url_small": "http://static.px.yelp.com/upthumb/ZQDXkIwQmgfAcazw8OgK2g/ss",
"url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:t-sisM24K9GvvYhr-9w1EQ",
"user_url": "http://yelp.com/user_details?userid=XMeRHjiLhA9cv3BsSOazCA",
"user_photo_url": "http://static.px.yelp.com/upthumb/ZQDXkIwQmgfAcazw8OgK2g/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_3.png",
"id": "t-sisM24K9GvvYhr-9w1EQ",
"text_excerpt": "So I know gentrification is supposed to be a bad word and all (especially here in SF), but the Lower Haight might benefit a bit from it. At least, I like...",
"user_name": "Trey F.",
"mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=t-sisM24K9GvvYhr-9w1EQ",
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_3.png"
},
{
"rating": 4,
"user_photo_url_small": "http://static.px.yelp.com/upthumb/Ghwoq23_alkaXawgqj7dBA/ss",
"url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:8xTNOC9L5ZXwGCMNYY-pdQ",
"user_url": "http://yelp.com/user_details?userid=4F2QG3adYIUNXplqqp9ylA",
"user_photo_url": "http://static.px.yelp.com/upthumb/Ghwoq23_alkaXawgqj7dBA/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_4.png",
"id": "8xTNOC9L5ZXwGCMNYY-pdQ",
"text_excerpt": "This place was definitely a great place to chill. The atmosphere is very non-threatening and very neighborly. I thought it was cool that they had a girl dj...",
"user_name": "Jessy M.",
"mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=8xTNOC9L5ZXwGCMNYY-pdQ",
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_4.png"
},
{
"rating": 5,
"user_photo_url_small": "http://static.px.yelp.com/upthumb/q0POOE3vv2LzNg1qN8MMyw/ss",
"url": "http://yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ#hrid:pp33WfN_FoKlQKJ-38j_Ag",
"user_url": "http://yelp.com/user_details?userid=FmcKafW272uSWXbUF2rslA",
"user_photo_url": "http://static.px.yelp.com/upthumb/q0POOE3vv2LzNg1qN8MMyw/ms",
"rating_img_url_small": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_small_5.png",
"id": "pp33WfN_FoKlQKJ-38j_Ag",
"text_excerpt": "Love this place! I've been here twice now and each time has been a great experience. The bartender is so nice. When we had questions about the drinks he...",
"user_name": "Scott M.",
"mobile_uri": "http://mobile.yelp.com/biz/yyqwqfgn1ZmbQYNbl7s5sQ?srid=pp33WfN_FoKlQKJ-38j_Ag",
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_5.png"
}
],
"phone": "4152550300",
"neighborhoods": [
{
"name": "Hayes Valley",
"url": "http://yelp.com/search?find_loc=Hayes+Valley%2C+San+Francisco%2C+CA"
}
],
"rating_img_url": "http://static.px.yelp.com/static/20070816/i/ico/stars/stars_4.png",
"longitude": -122.429926,
"categories": [
{
"name": "Dance Clubs",
"category_filter": "danceclubs",
"search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=danceclubs"
},
{
"name": "Lounges",
"category_filter": "lounges",
"search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=lounges"
},
{
"name": "American (Traditional)",
"category_filter": "tradamerican",
"search_url": "http://yelp.com/search?find_loc=466+Haight+St%2C+San+Francisco%2C+CA&cflt=tradamerican"
}
],
"state": "CA",
"review_count": 32,
"distance": 1.87804019451141
}
]

The first thing I notice off the bat is your use of
response['businesses'].length.times do |i|
# the business you want is response['businesses'][i]
end
for iterating. This can be simplified greatly by using Array.each which provides you with:
response['businesses'].each do |businessEntry|
# here, businessEntry is the actual object, so forego something like
# response['business'][businessEntry], just use businessEntry directly
end
You're actually doing the same thing below with your:
response['businesses'][businessEntry].each
Note on style, it's often (though no enforced) common style do use do/end if your block is on multiple lines and {} if the block is one line.
Also, not sure why you're building these key/value pairs in a string, just make a hash. Then you can easily convert to json, or as Jorg pointed out, just convert the whole response to json which does EXACTLY what you're doing manually... unless you need to work with the data first (which it doesn't look like you need to do)

I would be interested to see the error you were getting from hash.to_json to see if the cause for that can be found.
In terms of your Ruby code a couple of observations:
The use of .length.times do .. is a
bit odd when you could just use
each e.g.
response['businesses'].each do ..
In your else case
text+="\"#{arrayKey}\":\"#{arrayValue}\","
it looks like arrayKey and
arrayValue are out of scope because
they are only used as the block
variables in the each above.
text ="" sets text back to empty
string at each iteration of the outer
look so as the code stands it looks
like the text built up by the
previous iterations of the loop is
discarded.

I'm no expert on Ruby, but I do know what things that if they appear in my code, my professor yells at me for. Mikej already hit on the major things, especially with the use of #each instead of #length and #times.
If I am ever iterating through a collection of some sort, the only time I ever use something other than #each is when I need to use a custom iterator, and even then you could still use #each, but you would have to have a control statement inside the passed block to make sure that the block does not execute on certain instances. Even if it gets too complicated for that, the only other method of custom iteration I really use is a for i in Range.new( begin, end, optional_exclusion ) statement. And this could still be turned into a conditional in a block for #each, but it saves me code sometimes and makes it more explicit that I am intentionally not executing code on all elements of a collection as well as explicitly showing that I am setting the boundaries of the affected elements at the time of entry into the loop instead of hard-coded values or just the entire collection.
Mikej already pointed out the out the error of scope when calls to arrayKey and arrayValue are made in the else part of the if statement so I won't bother with that. He also already pointed out, you should probably move your text ="" code line up by two lines to get out of that response code block scope.
My only concern after that is some problems not with the code itself, but more sort of coding style and things generally practiced in the Ruby community. So these suggestions, by no means, you have to take. It just makes reading Ruby code generally easier.
So my first suggestion is whenever you call a method you are passing a block to, unless you can fit the proper indentation, object call, method call, block variable declaration, the code block, and the block closing all on the same line, then don't use curly braces. In those situations of multi-line code blocks, use the keywords do and end instead.
My second suggestion is to use proper indentation which in the Ruby language is two spaces instead of the normal four found in the coding styles of many other languages. You had proper indentation for a good chuck of your code and then you messed up and it kind caused some lines down the road to look like they are on a scope level that they are not. Now, this tip is nowhere near a coding style standard, but my general trick to make sure I am on the right scope level is just adding a end-line comment right after the use of the end keyword and put in the name of the scope it just closed. It has saved me from a number of scope bugs but I have never heard of anyone else doing it and it can possibly clutter code with these random end-line comments but it's a method that has served me well.
My third suggestion is to improve your use of strings and symbols. I am almost afraid to say this just because I still need to improve my understanding of symbols in Ruby and I don't recall using the Yelp class in any recent scripts so I am blind on that one. However, it looks like you use the string 'businesses' like a Hash key. A general rule in Ruby is if you are using a string just for what it represents, then you should be using a symbol while if you are using a string because you actually need its character contents, then you should probably stick to using a string. This is just because every time you call a string literal, a new string is made and stored in system memory. So here since you are using 'businesses' inside an each block that is inside and each block, you are potentially allocating that string O(n²) times (although the allocations for the inner block get garbage collected during the next iteration. Whereas if you used a symbol like ':businesses' instead, it only initializes it once. Also in the case of you text ="" line, you should modify that into a single quotation string literal. The Ruby interpreter can parse through single quote string literals faster than double quotes, so generally if you can use a single quoted string literal, do it.
All I got for suggestions. Take them as you need them or want them. Also here would be what your code would look like assuming you took my suggestions and I didn't break anything in the process.
def mineLocation
client = Yelp::Client.new
request = Yelp::Review::Request::GeoPoint.new(:latitude=>13.3125,
:longitude => -6.2468,
:yws_id => 'nicetry')
response = client.search(request)
text = ''
response[:businesses].each do |businessEntry|
response[:businesses][businessEntry].each do |key, value|
if value.kindOf( Array )
value.each do |arrayEntry|
text += "\"#{key}\":["
value[arrayEntry].each do |arrayKey, arrayValue|
text += "{\"#{arrayKey}\":\"#{arrayValue}\"},"
end #each
text += ']'
end #each
else
# Didn't fix because I didn't know you intentions here.
text += "\"#{arrayKey}\":\"#{arrayValue}\","
end #if
end #each
end #each
end #def
I didn't replace 'nicetry' with a symbol just because I don't know how the Yelp class works and might explicitly need a string there instead of a symbol. I also didn't know what the intended code effect was since the only time this code is executed is when the variables are out of scope, so I had no way of knowing what you were trying to reference in that line. Especially since at that point your value is also not an array either.
I know this is a long answer but I hope some of this helps!

Related

How to produce a concatenated string from a hash recursively?

I have the following complicated hash structure (among many) that looks like the following:
hash = {"en-us"=>
{:learn_more=>"Learn more",
:non_apple_desktop=>"To redeem, open this link.",
:value_prop_safety=>"",
:storage_size=>
{:apple_tv_1_month_tms=>
{:cta=>"Offer",
:details=>"Get a 1-month subscription!.",
:disclaimer=>"This offer expires on December 10, 2021.",
:header=>"Watch The Morning Show ",
:normal_price=>"$2.99"}
}
}
}
What I'd like to do is to have a function that will produce the following string output based off the hash structure:
en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety:
I've used this recursive function from another stackoverflow question that somewhat accomplishes this:
def show(hash, current_path = '')
string = ''
hash.each do |k,v|
if v.respond_to?(:each)
current_path += "#{k}."
show v, current_path
else
string += "#{current_path}#{k}: #{v}" + "\n"
end
end
string
end
If I place a puts statement in the body of the method I can see the desired result but its line by line. What I need is to obtain the entirety of the output because I will be writing it to a csv. I can't seem to get it to work in its current incarnation.
If I were to place a puts show(hash) into my irb, then I won't get any output. So in summary, I am trying to do the following:
show(hash) ----->
en-us.storage_size.apple_tv_1_month_tms.cta: Offer
en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
en-us.learn_more: Learn more
en-us.non_apple_desktop: To redeem, open this link.
en-us.value_prop_safety:
This should be an easy recursive task but I can't pinpoint what exactly I've got wrong. Help would be greatly appreciated. Thank you.
In my opinion it is much more convenient to use i18n gem
It has I18n::Backend::Flatten#flatten_translations method. It receives a hash of translations (where the key is a locale and the value is another hash) and return a hash with all translations flattened, just as you need
Just convert the resulting hash to a string and you're done
require "i18n/backend/flatten"
include I18n::Backend::Flatten
locale_hash = {"en-us"=>
{:learn_more=>"Learn more",
:non_apple_desktop=>"To redeem, open this link.",
:value_prop_safety=>"",
:storage_size=>
{:apple_tv_1_month_tms=>
{:cta=>"Offer",
:details=>"Get a 1-month subscription!.",
:disclaimer=>"This offer expires on December 10, 2021.",
:header=>"Watch The Morning Show ",
:normal_price=>"$2.99"}
}
}
}
puts flatten_translations(nil, locale_hash, nil, nil).
map { |k, v| "#{k}: #{v}" }.
join("\n")
# will print
# en-us.learn_more: Learn more
# en-us.non_apple_desktop: To redeem, open this link.
# en-us.value_prop_safety:
# en-us.storage_size.apple_tv_1_month_tms.cta: Offer
# en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.
# en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.
# en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show
# en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99
Of course it's better to include not in main object, but in some service object
require "i18n/backend/flatten"
class StringifyLocaleHash
include I18n::Backend::Flatten
attr_reader :locale_hash
def self.call(locale_hash)
new(locale_hash).call
end
def initialize(locale_hash)
#locale_hash = locale_hash
end
def call
flatten_translations(nil, locale_hash, nil, nil).
map { |k, v| "#{k}: #{v}" }.
join("\n")
end
end
# To get string call such way
StringifyLocaleHash.(locale_hash)
To answer your literal question:
show v, current_path
should be
string += show v, current_path
otherwise you lose any work that your recursive call has done.
Note that a += b replaces a with the new string a + b. It does not change a. Thus, preserving the return value of show is critical.
If you want to rely on strings being mutable, here is a mutable string version; however, note that it may not always work, since string immutability is an option added to Ruby. If frozen_string_literals is on, the mutable concatenation operator << will fail. In the mutable string version, you can't initialise string in each iteration, because you'd be discarding the work your caller has done; so it is passed as another parameter, and initialised by the default value only on its initial call.
def show(hash, current_path = '', string = '')
hash.each do |k,v|
if v.respond_to?(:each)
current_path += "#{k}."
show v, current_path, string
else
string << "#{current_path}#{k}: #{v}" + "\n"
end
end
string
end
Considerations on the Approach
First, some issues:
Ruby is not JavaScript. Hashes don't have properties, so chained dots aren't going to work unless you do a lot of work to split and join the paths to your data after the fact. The juice is probably not worth the squeeze.
en-us is not a valid LANG value. You can use it a a key, but you probably want something more technically accurate like en_US or en_US.UTF-8 as a key if the locale matters.
Secondly, if you already know the structure of your JSON, then you should just make each JSON key a column value and populate it. This is likely a lot easier.
Thirdly, while Ruby can do recursion, most of its built-in methods are designed for iteration. Iterating is faster and easier for a lot of things, so unless you're doing this for other reasons, just iterate over the information you need.
Building CSV from Structured Data with a Predefined Format
Finally, if you just want the data to populate a CSV or create strings, then maybe you just want to extract the data and then munge it with information you already know anyway. For example:
require "hashie"
# assume your hash is already defined
offer_values =
hash.extend(Hashie::Extensions::DeepFind)
.deep_find(:apple_tv_1_month_tms).values
#=>
# ["Offer",
# "Get a 1-month subscription!.",
# "This offer expires on December 10, 2021.",
# "Watch The Morning Show ",
# "$2.99"]
To get the values of the top-level keys, you can can do it like this:
linking = hash["en-us"].keys[0..-2].map { hash.dig "en-us", _1 }
#=> ["Learn more", "To redeem, open this link.", ""]
With more work, you could do similar things with ActiveSupport or by using Ruby 3.1 pattern matching with a find pattern, but this will work on older Ruby versions too. The only downside is that it relies on the Hashie gem for Hashie::DeepFind, but this is one of those cases where Ruby doesn't have a simple built-in method for finding nested structures other than pattern matching and I think it's worth it.
You'll still have to do some work to convert the extracted data into the exact format you want, but this gets you all the values you're after into a pair of variables, offer_values and linking. You can then do whatever you want with them.
See Also
In addition to Hashie::DeepFind and Ruby 3 pattern matching, you also have a number of query languages that might be useful in extracting data from JSON, some of which have Ruby gems or can be easily integrated in your Ruby application:
JSONPath
https://github.com/joshbuddy/jsonpath
https://www.ruby-toolbox.com/projects/remap
XPath 3.1+
XQuery 3.1+
This is a relatively straightforward recursion as each value is a string or a hash. It can be written as follows.
def recurse(h)
h.each_with_object([]) do |(k,v),a|
case v
when Hash
recurse(v).each { |ss| a << "#{k}.#{ss}" }
else
a << "#{k}: #{v}"
end
end
end
recurse hash
#=> ["en-us.learn_more: Learn more",
# "en-us.non_apple_desktop: To redeem, open this link.",
# "en-us.value_prop_safety: ",
# "en-us.storage_size.apple_tv_1_month_tms.cta: Offer",
# "en-us.storage_size.apple_tv_1_month_tms.details: Get a 1-month subscription!.",
# "en-us.storage_size.apple_tv_1_month_tms.disclaimer: This offer expires on December 10, 2021.",
# "en-us.storage_size.apple_tv_1_month_tms.header: Watch The Morning Show ",
# "en-us.storage_size.apple_tv_1_month_tms.normal_price: $2.99"]

Metaprogramming Ruby 2 example from the book isn't working when I try it. Trouble shooting assistance

While reading through a chapter of Metaprogramming Ruby 2, I've come across an example in the book that does not seem to work when I execute the code.
array_explorer.rb
def explore_array(method)
code = "['a','b','c'].#{method}"
puts "Evaluating: #{code}"
eval code
end
loop { p explore_array(gets()) }
The code above is designed to illustrate the power of eval. In the next example the book teaches the major flaw of code injections and refactors the code like so to safeguard:
array_explorer.rb
def explore_array(method, *arguments)
['a','b','c'].send(method, *arguments)
end
loop { p explore_array(gets()) }
When I try to run the above code, the file always gives me this error no matter what array method I try to place in.
array_explorer.rb:2:in `explore_array': undefined method `:size (NoMethodError)
' for ["a", "b", "c"]:Array
I've tried taking out the *arguments portion to whittle it down. I tried using a string as input, a symbol as input, etc. This code doesn't work for some reason. Does anyone know why?
gets reads a line from STDIN; a "line" is defined as a string of characters ending with a newline (\n). Thus, you are trying to invoke the method "size\n", which does not exist. Use chomp to get rid of the newline:
loop { p explore_array(gets.chomp) }
It does not matter in the first example, since you are evaluating the code "['a', 'b', 'c'].size\n", which is still valid.

Push an array into another array with Ruby, and return square brackets

I've spent a few hours searching for a way to push an array into another array or into a hash. Apologies in advance if the formatting of this question is bit messy. This is the first time I've asked a question on StackOverflow so I'm trying to get the hang of styling my questions properly.
I have to write some code to make the following test unit past:
class TestNAME < Test::Unit::TestCase
def test_directions()
assert_equal(Lexicon.scan("north"), [['direction', 'north']])
result = Lexicon.scan("north south east")
assert_equal(result, [['direction', 'north'],
['direction', 'south'],
['direction', 'east']])
end
end
The most simple thing I've come up with is below. The first part passes, but then the second part is not returning the expected result when I run rake test.
Instead or returning:
[["direction", "north"], ["direction", "south"], ["direction",
"east"]]
it's returning:
["north", "south", "east"]
Although, if I print the result of y as a string to the console, I get 3 separate arrays that are not contained within another array (as below). Why hasn't it printed the outermost square brackets of the array, y?
["direction", "north"]
["direction", "south"]
["direction", "east"]
Below is the code I've written in an attempt to pass the test unit above:
class Lexicon
def initialize(stuff)
#words = stuff.split
end
def self.scan(word)
if word.include?(' ')
broken_words = word.split
broken_words.each do |word|
x = ['direction']
x.push(word)
y = []
y.push(x)
end
else
return [['direction', word]]
end
end
end
Any feedback about this will be much appreciated. Thank you all so much in advance.
What you're seeing is the result of each, which returns the thing being iterated over, or in this case, broken_words. What you want is collect which returns the transformed values. Notice in your original, y is never used, it's just thrown out after being composed.
Here's a fixed up version:
class Lexicon
def initialize(stuff)
#words = stuff.split
end
def self.scan(word)
broken_words = word.split(/\s+/)
broken_words.collect do |word|
[ 'direction', word ]
end
end
end
It's worth noting a few things were changed here:
Splitting on an arbitrary number of spaces rather than one.
Simplifying to a single case instead of two.
Eliminating the redundant return statement.
One thing you might consider is using a data structure like { direction: word } instead. That makes referencing values a lot easier since you'd do entry[:direction] avoiding the ambiguous entry[1].
If you're not instantiating Lexicon objects, you can use a Module which may make it more clear that you're not instantiating objects.
Also, there is no need to use an extra variable (i.e. broken_words), and I prefer the { } block syntax over the do..end syntax for functional blocks vs. iterative blocks.
module Lexicon
def self.scan str
str.split.map {|word| [ 'direction', word ] }
end
end
UPDATE: based on Cary's comment (I assume he meant split when he said scan), I've removed the superfluous argument to split.

What does |lang| part mean in this each method? Where does this come from?

I am reading through Chris Pine's Learn To Program chapter 7 Arrays and Iterators.
He introduces the each method with the following example:
languages = ['English', 'German', 'Ruby']
languages.each do |lang|
puts 'I love ' + lang + '!'
puts 'Don\'t you?'
end
puts 'And let\'s hear it for C++!'
puts '...'
It's not hard to understand how it works overall, but I can't figure out where the |lang| part is coming from so out of blue. Shouldn't it be assigned/named or something before it can be used like this? So the computer can know what the "lang" refers to? Does || do something wrapping around lang? Or does ruby just know what lang means?
I am afraid the question is too basic, but I am hoping someone might help me just a bit...
lang is a variable used to hold an element from the languages array. Any variable inside || will be used to grab single element from array. So, every time the loops executes, an element from the array is popped out and held in an variable named lang and data held by lang is displayed using puts method.
The each method yields every element one by one and it gets assigned to the variable lang.
Internally, the each method is implemented something like this:
def each
index = 0
while index < array.length
yield array[index]
index += 1
end
end
|lang| is a block variable. If you strip down your code, you can see that the .each method is iterating over the languages array and assigning array elements to the block variable:
languages = ['English', 'German', 'Ruby']
languages.each do |lang|
puts lang
end
#=> English
#=> German
#=> Ruby
Multi-line blocks use a do/end syntax (as in your example), and single-line blocks use a braces syntax. For example:
languages = ['English', 'German', 'Ruby']
languages.each { |lang| puts lang}
It sounds like, in the above example, you created an array storing multiple language variables.
You then iterated over all three elements in the array and represented each one with a variable called lang.
lang, which is inside the brackets is simply a variable.
Hope this helped you

Is there a way to pass a regex capture to a block in Ruby?

I have a hash with a regex for the key and a block for the value. Something like the following:
{ 'test (.+?)' => { puts $1 } }
Not exactly like that, obviously, since the block is being stored as a Proc, but that's the idea.
I then have a regex match later on that looks a lot like this
hash.each do |pattern, action|
if /#{pattern}/i.match(string)
action.call
end
end
The idea was to store the block away in the hash to make it a bit easier for me to expand upon in the future, but now the regex capture doesn't get passed to the block. Is there a way to do this cleanly that would support any number of captures I put in the regex (as in, some regex patterns may have 1 capture, others may have 3)?
What if you pass the match data into your procs?
hash.each do |pattern, action|
if pattern.match(string)
action.call($~)
end
end
Your hash would become:
{ /test (.+?)/i => lambda { |matchdata| puts matchdata[1] } }
I would use Hash.find which walks the hash elements, passing them into a block, one at a time. The one that returns true wins:
Something like this:
hash = {/foo/ => lambda { 'foo' }, /bar/ => lambda { 'bar' } }
str = 'foo'
puts hash.find{ |n,v| str =~ n }.to_a.last.call
Obviously I'm using lambda but it's close enough. And, if there was no match you need to handle nil values. For the example I chained to_a.last.call but in real life you'd want to react to a nil otherwise Ruby will get mad.
If you are searching through a lot of patterns, or processing a lot of text, your search will be slowed down by having to recompile the regex each time. I'd recommend storing the keys as regex objects to avoid that.

Resources