ruby change content of a string - ruby-on-rails

I'm doing data processing, one task is to get stats of people distribution. Say for the people of name "john doe", there fall in different states, ca, ar, and ny, and of different age groups, twenties, thirties, etc. {1,2} or {3} is the people's id.
"john doe" => "ca:tw#2{1,2}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};"
Now if I want to get the id of john doe in ca with age tw, how should I get them? Maybe using Regex? And if I want to add a new id to it, say 100, now it becomes
"john doe" => "ca:tw#3{1,2,100}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};"
how should I do that?
Thanks!

If you want to stick with string manipulation, you can use regex and gsub.
Here is one way to do it. It could use some clean up (eg error handling, re-factoring, etc.), but I think it would get you started.
def count(details, location, age_group)
location_details = /#{location}(.+?);/.match(details)[1]
age_count = /#{age_group}#(\d+)\{/.match(details)[1]
return age_count.to_i
end
def ids(details, location, age_group)
location_details = /#{location}(.+?);/.match(details)[1]
age_ids = /#{age_group}#\d+\{(.+?)\}/.match(details)[1]
return age_ids
end
def add(details, location, age_group, new_id)
location_details = /#{location}(.+?);/.match(details)[1]
new_count = count(details, location, age_group) + 1
new_ids = ids(details, location, age_group) + ',' + new_id
location_details.gsub!(/#{age_group}#\d+\{(.+?)\}/, "#{age_group}##{new_count}{#{new_ids}}")
details.gsub!(/#{location}(.+?);/, "#{location}#{location_details};")
end
You can see it produces the results you wanted (at least functionally, not sure about performance):
names = {"john doe" => "ca:tw#2{1,2}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};"}
puts count(names["john doe"], 'ca', 'tw')
#=> 2
puts ids(names["john doe"], 'ca', 'tw')
#=> 1,2
names["john doe"] = add(names["john doe"], 'ca', 'tw', '100')
puts names["john doe"]
#=> ca:tw#3{1,2,100}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};

It doesn't make sense to use a string for this inside the program. You may read the data from a string as it is stored, or write it back out that way, but you should store it in a manner that's easy to manipulate. For instance:
data = {
"john doe" => {
"ca" => {
"tw" => [1,2],
"th" => [3]
},
"ar" => {
"tw" => [4],
"fi" => [5]
},
"ny" => {
"tw" => [6]
}
}
}
Given that, the ids of the California John Doe's in their 20's are data['john doe']['ca']['tw']. The number of such John Doe's is data['john doe']['ca']['tw'].length; the first id is data['john doe']['ca']['tw'][0], and the second is data['john doe']['ca']['tw'][1]. You could add id 100 to it with data['john doe']['ca']['tw'] << 100; 100 would then be the value of data['john doe']['ca']['tw'][2].
If I were writing this, though, I would probably use actual numbers for the age-range keys (20, 30, 50) instead of those obscure letter prefixes.

Related

Checking if hash value has a text

I have a hash:
universityname = e.university
topuniversities = CSV.read('lib/assets/topuniversities.csv',{encoding: "UTF-8", headers:true, header_converters: :symbol, converters: :all})
hashed_topuniversities = topuniversities.map {|d| d.to_hash}
hashed_topuniversities.any? {|rank, name| name.split(' ').include?(universityname) }.each do |s|
if s[:universityrank] <= 10
new_score += 10
elsif s[:universityrank] >= 11 && s[:universityrank] <= 25
new_score += 5
elsif s[:universityrank] >= 26 && s[:universityrank] <= 50
new_score += 3
elsif s[:universityrank] >= 51 && s[:universityrank] <= 100
new_score += 2
end
Basically what this is doing is looking at a hash and checking if the hash value contains a university name is an input.
For example the user input can be "Oxford University" and in the hash its stored as "Oxford". The User needs to type in as it stored in the hash to be able to be assigned a score, But I want it that if the user types in "oxford university" then the hash value "Oxford" should be selected and then go through.
Everything else in this works fine but the .include? does not work correctly, I still need to type the exact word.
hashed_topuniversities = topuniversities.map &:to_hash
univ = hashed_topuniversities.detect do |rank, name|
name.downcase.split(' ').include?(universityname.downcase)
end
new_score += case univ[:universityrank]
when -Float::INFINITY..10 then 10
when 11..25 then 5
when 26..50 then 3
when 50..100 then 2
else 0
end
Besides some code improvements in terms of being more idiomatic ruby, the main change is downcase called on both university name and user input. Now they are compared case insensitive.
I don't think your approach will work (in real-life, anyway). "University of Oxford" is an easy one--just look for the presence of the word, "Oxford". What about "University of Kansas"? Would you merely try to match "Kansas"? What about "Kansas State University"?
Also, some universities are are customarily referred to by well-know acronyms or shortened names, such as "LSE", "UCLA", "USC", "SUNY", "LSU", "RPI", "Penn State", "Georgia Tech", "Berkeley" and "Cal Tech". You also need to think about punctuation and "little words" (e.g., "at", "the", "of") in university names (e.g., "University of California, Los Angeles").
For any serious application, I think you need to construct a list of all commonly-used names for each university and then require an exact match between those names and the given university name (after punctuation and little words have been removed). You can do that by modifying the hash hashed_top_universities, perhaps like this:
hashed_top_universities
#=> { "University of California at Berkeley" =>
# { rank: 1, names: ["university california", "berkeley", "cal"] },
# "University of California at Los Angeles" =>
# { rank: 2, names: ["ucla"] },
# "University of Oxford" =>
# { rank: 3, names: ["oxford", "oxford university"] }
# }
Names of some universities contain non-ASCII characters, which is a further complication (that I will not address).
Here's how you might code it.
Given a university name, the first step is to construct a hash (reverse_hash) that maps university names to ranks. The names consist of the elements of the value of the key :names in the inner hashes in hashed_top_universities, together with the complete university names that comprise the keys in that hash, after they have been downcased and punctuation and "little words" have been removed.
PUNCTUATION = ",."
EXCLUSIONS = %w| of for the at u |
SCORE = { 1=>10, 3=>7, 25=>5, 50=>3, 100=>2, Float::INFINITY=>0 }
reverse_hash = hashed_top_universities.each_with_object({}) { |(k,v),h|
(v[:names] + [simplify(k)]).each { |name| h[name] = v[:rank] } }
#=> {"university california"=>1, "berkeley"=>1, "cal"=>1,
# "university california berkeley"=>1,
# "ucla"=>2, "university california los angeles"=>2,
# "oxford"=>3, "oxford university"=>3, "university oxford"=>3}
def simplify(str)
str.downcase.delete(PUNCTUATION).
gsub(/\b#{Regexp.union(EXCLUSIONS)}\b/,'').
squeeze(' ')
end
def score(name, reverse_hash)
rank = reverse_hash[simplify(name)]
SCORE.find { |k,_| rank <= k }.last
end
Let's try it.
score("University of California at Berkeley", reverse_hash)
#=> 10
score("Cal", reverse_hash)
#=> 10
score("UCLA", reverse_hash)
#=> 7
score("Oxford", reverse_hash)
#=> 7

How do I search a Ruby hash from an array of values, and then return a hash based on the findings?

Here's an example hash and an example array to search in the hash:
nicknames = { "Black Mamba" => "Kobe Bryant",
"Half Man Half Amazing" => "Vince Carter",
"The Big Fundamental" => "Tim Duncan",
"Big Ticket" => "Kevin Garnett",
"Obi-Wan Ginobili" => "Manu Ginobili",
"The Answer" => "Allen Iverson" }
names = [ "Vince Carter", "Manu Ginobili", "Allen Iverson" ]
I want to return:
selected = { "Half Man Half Amazing" => "Vince Carter", "Obi-Wan Ginobili" => "Manu Ginobili", "The Answer" = "Allen Iverson" }
What's a good way to do this? Thanks!
You can simply do the following:
nicknames.select { |key, value| names.include?(value) }
(copy-paste the code you provided and mine in your IRB console and you'll see it working).
If the values in the hash are unique, then you can reverse the keys and the values. MrYoshiji's code works, but here is a more efficient way.
hash = nicknames.invert
names.to_enum.with_object({}){|k, h| h[hash[k]] = k}

How can you sort an array in Ruby starting at a specific letter, say letter f?

I have a text array.
text_array = ["bob", "alice", "dave", "carol", "frank", "eve", "jordan", "isaac", "harry", "george"]
text_array = text_array.sort would give us a sorted array.
However, I want a sorted array with f as the first letter for our order, and e as the last.
So the end result should be...
text_array = ["frank", "george", "harry", "isaac", "jordan", "alice", "bob", "carol", "dave", "eve"]
What would be the best way to accomplish this?
Try this:
result = (text_array.select{ |v| v =~ /^[f-z]/ }.sort + text_array.select{ |v| v =~ /^[a-e]/ }.sort).flatten
It's not the prettiest but it will get the job done.
Edit per comment. Making a more general piece of code:
before = []
after = []
text_array.sort.each do |t|
if t > term
after << t
else
before << t
end
end
return (after + before).flatten
This code assumes that term is whatever you want to divide the array. And if an array value equals term, it will be at the end.
You can do that using a hash:
alpha = ('a'..'z').to_a
#=> ["a", "b", "c",..."x", "y", "z"]
reordered = alpha.rotate(5)
#=> ["f", "g",..."z", "a",...,"e"]
h = reordered.zip(alpha).to_h
# => {"f"=>"a", "g"=>"b",..., "z"=>"u", "a"=>"v",..., e"=>"z"}
text_array.sort_by { |w| w.gsub(/./,h) }
#=> ["frank", "george", "harry", "isaac", "jordan",
# "alice", "bob", "carol", "dave", "eve"]
A variant of this is:
a_to_z = alpha.join
#=> "abcdefghijklmnopqrstuvwxyz"
f_to_e = reordered.join
#=> "fghijklmnopqrstuvwxyzabcde"
text_array.sort_by { |w| w.tr(f_to_e, a_to_z) }
#=> ["frank", "george", "harry", "isaac", "jordan",
# "alice", "bob", "carol", "dave", "eve"]
I think the easiest would be to rotate the sorted array:
text_array.rotate(offset) if offset = text_array.find_index { |e| e.size > 0 and e[0] == 'f' }
Combining Ryan K's answer and my previous answer, this is a one-liner you can use without any regex:
text_array = text_array.sort!.select {|x| x.first >= "f"} + text_array.select {|x| x.first < "f"}
If I got your question right, it looks like you want to create sorted list with biased predefined patterns.
ie. let's say you want to define specific pattern of text which can completely change the sorting sequence for the array element.
Here is my proposal, you can get better code out of this, but my tired brain got it for now -
an_array = ["bob", "alice", "dave", "carol", "frank", "eve", "jordan", "isaac", "harry", "george"]
# Define your patterns with scores so that the sorting result can vary accordingly
# It's full fledged Regex so you can put any kind of regex you want.
patterns = {
/^f/ => 100,
/^e/ => -100,
/^g/ => 60,
/^j/ => 40
}
# Sort the array with our preferred sequence
sorted_array = an_array.sort do |left, right|
# Find score for the left string
left_score = patterns.find{ |p, s| left.match(p) }
left_score = left_score ? left_score.last : 0
# Find the score for the right string
right_score = patterns.find{ |p, s| right.match(p) }
right_score = right_score ? right_score.last : 0
# Create the comparision score to prepare the right order
# 1 means replace with right and -1 means replace with left
# and 0 means remain unchanged
score = if right_score > left_score
1
elsif left_score > right_score
-1
else
0
end
# For debugging purpose, I added few verbose data
puts "L#{left_score}, R:#{right_score}: #{left}, #{right} => #{score}"
score
end
# Original array
puts an_array.join(', ')
# Biased array
puts sorted_array.join(', ')

rails, inserting into hash then counting occurrences logic

i need to create a hash/array where 2 elements are stored: the country code, and the number of times the country occurred.
I want to vet some conceptual logic: i want to create a helper method that passes in a list of countries. Then, I loop through each country and will merge the country code into the hash through a series of if statements:
#map_country = Hash.new
if country == "United States"
#map_country.merge(:us => ??)
I'm not quite sure how I can add a counter to push into the hash. Can anyone help? Basically, I want to achieve how many times "United States" shows up.
Also, once I have this Hash completed - I want to do something different to each country based on the count. How do I go about picking out the value from the key? Moreover, how do I get just the key?
<% if #map_country[:country] > 5 %>
... do this with #map_country...
Thanks! Apologies if this is confusing, but really could use some help here. Thanks!
To me it sounds like you're trying to count occurrences which you can do with the #inject method:
[1] pry(main)> countries = ["United States", "Canada", "United States", "Mexico"]
=> ["United States", "Canada", "United States", "Mexico"]
[2] pry(main)> countries.inject({}) { |hash, ctr| hash[ctr] = hash[ctr].to_i + 1; hash }
=> {"United States"=>2, "Canada"=>1, "Mexico"=>1}
Then say you want to do something with that hash, you could loop through it like this:
[3] pry(main)> occ = countries.inject({}) { |hash, ctr| hash[ctr] = hash[ctr].to_i + 1; hash }
=> {"United States"=>2, "Canada"=>1, "Mexico"=>1}
[4] pry(main)> occ.each do |country, val|
[4] pry(main)* if val == 2
[4] pry(main)* puts "There are two occurences of #{country}"
[4] pry(main)* end
[4] pry(main)* end
There are two occurences of United States
If you're set on using a Hash (rather than a custom class) for this then just use a default_proc to auto-vivify entries with zeros and you a simple increment is all you need:
#map_country = Hash.new { |h, k| h[k] = 0 }
if country == 'United States'
#map_country[:us] += 1

Ruby Methods and Optional parameters

I'm playing with Ruby on Rails and I'm trying to create a method with optional parameters. Apparently there are many ways to do it. I trying naming the optional parameters as hashes, and without defining them. The output is different. Take a look:
# This functions works fine!
def my_info(name, options = {})
age = options[:age] || 27
weight = options[:weight] || 160
city = options[:city] || "New York"
puts "My name is #{name}, my age is #{age}, my weight is #{weight} and I live in {city}"
end
my_info "Bill"
-> My name is Bill, my age is 27, my weight is 160 and I live in New York
-> OK!
my_info "Bill", age: 28
-> My name is Bill, my age is 28, my weight is 160 and I live in New York
-> OK!
my_info "Bill", weight: 200
-> My name is Bill, my age is 27, my weight is 200 and I live in New York
-> OK!
my_info "Bill", city: "Scottsdale"
-> My name is Bill, my age is 27, my weight is 160 and I live in Scottsdale
-> OK!
my_info "Bill", age: 99, weight: 300, city: "Sao Paulo"
-> My name is Bill, my age is 99, my weight is 300 and I live in Sao Paulo
-> OK!
****************************
# This functions doesn't work when I don't pass all the parameters
def my_info2(name, options = {age: 27, weight: 160, city: "New York"})
age = options[:age]
weight = options[:weight]
city = options[:city]
puts "My name is #{name}, my age is #{age}, my weight is #{weight} and I live in #{city}"
end
my_info2 "Bill"
-> My name is Bill, my age is 27, my weight is 160 and I live in New York
-> OK!
my_info2 "Bill", age: 28
-> My name is Bill, my age is 28, my weight is and I live in
-> NOT OK! Where is my weight and the city??
my_info2 "Bill", weight: 200
-> My name is Bill, my age is , my weight is 200 and I live in
-> NOT OK! Where is my age and the city??
my_info2 "Bill", city: "Scottsdale"
-> My name is Bill, my age is , my weight is and I live in Scottsdale
-> NOT OK! Where is my age and my weight?
my_info2 "Bill", age: 99, weight: 300, city: "Sao Paulo"
-> My name is Bill, my age is 99, my weight is 300 and I live in Sao Paulo
-> OK!
What's wrong with the second approach for optional parameters?
The second method only works if I don't pass any optional parameter or if I pass them all.
What am I missing?
The way optional arguments work in ruby is that you specify an equal sign, and if no argument is passed then what you specified is used. So, if no second argument is passed in the second example, then
{age: 27, weight: 160, city: "New York"}
is used. If you do use the hash syntax after the first argument, then that exact hash is passed.
The best you can do is
def my_info2(name, options = {})
options = {age: 27, weight: 160, city: "New York"}.merge(options)
...
The problem is the default value of options is the entire Hash in the second version you posted. So, the default value, the entire Hash, gets overridden. That's why passing nothing works, because this activates the default value which is the Hash and entering all of them also works, because it is overwriting the default value with a Hash of identical keys.
I highly suggest using an Array to capture all additional objects that are at the end of your method call.
def my_info(name, *args)
options = args.extract_options!
age = options[:age] || 27
end
I learned this trick from reading through the source for Rails. However, note that this only works if you include ActiveSupport. Or, if you don't want the overhead of the entire ActiveSupport gem, just use the two methods added to Hash and Array that make this possible.
rails / activesupport / lib / active_support / core_ext / array /
extract_options.rb
So when you call your method, call it much like you would any other Rails helper method with additional options.
my_info "Ned Stark", "Winter is coming", :city => "Winterfell"
If you want to default the values in your options hash, you want to merge the defaults in your function. If you put the defaults in the default parameter itself, it'll be over-written:
def my_info(name, options = {})
options.reverse_merge!(age: 27, weight: 160, city: "New York")
...
end
In second approach, when you say,
my_info2 "Bill", age: 28
It will pass {age: 28}, and entire original default hash {age: 27, weight: 160, city: "New York"} will be overridden. That's why it does not show properly.
You can also define method signatures with keyword arguments (New since, Ruby 2.0, since this question is old):
def my_info2(name, age: 27, weight: 160, city: "New York", **rest_of_options)
p [name, age, weight, city, rest_of_options]
end
my_info2('Joe Lightweight', weight: 120, age: 24, favorite_food: 'crackers')
This allows for the following:
Optional parameters (:weight and :age)
Default values
Arbitrary order of parameters
Extra values collected in a hash using double splat (:favorite_food collected in rest_of_options)
For the default values in your hash you should use this
def some_method(required_1, required_2, options={})
defaults = {
:option_1 => "option 1 default",
:option_2 => "option 2 default",
:option_3 => "option 3 default",
:option_4 => "option 4 default"
}
options = defaults.merge(options)
# Do something awesome!
end
To answer the question of "why?": the way you're calling your function,
my_info "Bill", age: 99, weight: 300, city: "Sao Paulo"
is actually doing
my_info "Bill", {:age => 99, :weight => 300, :city => "Sao Paulo"}
Notice you are passing two parameters, "Bill" and a hash object, which will cause the default hash value you've provided in my_info2 to be completely ignored.
You should use the default value approach that the other answerers have mentioned.
#fetch is your friend!
class Example
attr_reader :age
def my_info(name, options = {})
#age = options.fetch(:age, 27)
self
end
end
person = Example.new.my_info("Fred")
puts person.age #27
I don't see anything wrong with using an or operator to set defaults. Here's a real life example (note, uses rails' image_tag method):
file:
def gravatar_for(user, options = {} )
height = options[:height] || 90
width = options[:width] || 90
alt = options[:alt] || user.name + "'s gravatar"
gravatar_address = 'http://1.gravatar.com/avatar/'
clean_email = user.email.strip.downcase
hash = Digest::MD5.hexdigest(clean_email)
image_tag gravatar_address + hash, height: height, width: width, alt: alt
end
console:
2.0.0-p247 :049 > gravatar_for(user)
=> "<img alt=\"jim's gravatar\" height=\"90\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"90\" />"
2.0.0-p247 :049 > gravatar_for(user, height: 123456, width: 654321)
=> "<img alt=\"jim's gravatar\" height=\"123456\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"654321\" />"
2.0.0-p247 :049 > gravatar_for(user, height: 123456, width: 654321, alt: %[dogs, cats, mice])
=> "<img alt=\"dogs cats mice\" height=\"123456\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"654321\" />"
It feels similar to using the initialize method when calling a class.
Why not just use nil?
def method(required_arg, option1 = nil, option2 = nil)
...
end
There is a method_missing method on ActiveRecord models that you can override to have your class dynamically respond to calls directly. Here's a nice blog post on it.

Resources