Ruby dynamic variable name - ruby

is there any way to create variables in Ruby with dynamic names?
I'm reading a file and when I find a string, generates a hash.
e.g.
file = File.new("games.log", "r")
file.lines do |l|
l.split do |p|
if p[1] == "InitGame"
Game_# = Hash.new
end
end
end
How could I change # in Game_# to numbers (Game_1, Game_2, ...)

You can do it with instance variables like
i = 0
file.lines do |l|
l.split do |p|
if p[1] == "InitGame"
instance_variable_set("#Game_#{i += 1}", Hash.new)
end
end
end
but you should use an array as viraptor says. Since you seem to have just a new hash as the value, it can be simply
i = 0
file.lines do |l|
l.split do |p|
if p[1] == "InitGame"
i += 1
end
end
end
Games = Array.new(i){{}}
Games[0] # => {}
Games[1] # => {}
...

Why use separate variables? It seems like you just want Game to be a list with the values appended to it every time. Then you can reference them with Game[0], Game[1], ...

If you really want dynamic variable names, may be you can use a Hash, than your can set the key dynamic
file = File.new("games.log", "r")
lines = {}
i = 0
file.lines do |l|
l.split do |p|
if p[1] == "InitGame"
lines[:"Game_#{i}"] = Hash.new
i = i + 1
end
end
end

Related

Find the name and age of the oldest person in a txt file using ruby

"Attached is a file with people's names and ages.
There will always be a First name and Last name followed by a colon then the age.
So each line with look something like this.
FirstName LastName: Age
Your job is write a ruby program that can read this file and figure out who the oldest person/people are on this list. Your program should print out their name(s) and age(s)."
This is the code I have so far:
File.open('nameage.txt') do |f|
f.each_line do |line|
line.split(":").last.to_i
puts line.split(":").last.to_i
end
end
With this, I am able to separate the name from the age but I don't know how to get the highest value and print out the highest value with name and age.
Please help!
"figure out who the oldest person/people are on this list", so multiple results are possible. Ruby has a group_by method, which groups an enumerable by a common property. What property? The property you specify in the block.
grouped = File.open('nameage.txt') do |f|
f.group_by do |line|
line.split(":").last.to_i # using OP's line
end
end
p grouped # just to see what it looks like
puts grouped.max.last # end result
You could push all the ages into an array. Do array.max or sort the array and do array[-1].
Here's how I would approach it:
oldest_name = nil
oldest_age = 0
For each line in file do
split line at the colon and store the age inside age variable
split line at the colon and store the age inside name variable
if age is greater than oldest_age then
oldest_age = age
oldest_name = name
end
end
finally print the oldest_name and oldest_age
If you're in to one-liners try this
$ cat nameage.txt
John Doe: 34
Tom Jones: 50
Jane Doe: 32
Citizen Kane: 29
$ irb
1.9.3-p551 :001 > IO.read("nameage.txt").split("\n").sort_by { |a| a.split(":")[1].to_i }.last
=> "Tom Jones: 50"
You can try using hash also,
hash = {}
File.open('nameage.txt') do |f|
f.each_line do |line|
data = line.split(":")
hash[data.first] = data.last.strip
end
hash.max_by{|k,v| v}.join(" : ")
end
File.open('nameage.txt') do |handle|
people = handle.each_line.map { |line| line.split(":") }
oldest_age = people.map { |_, age| age.to_i }.max
people.select { |_, age| age.to_i == oldest_age }.each do |name, age|
puts "#{name}, #{age}"
end
end
You are going the right way. Now you just need to store the right things in the right places. I just merged your code and the code proposed by #oystersauce14.
oldest_name = nil
oldest_age = 0
File.open('nameage.txt') do |f|
f.each_line do |line|
data = line.split(":")
curr_name = data[0]
curr_age = data[1].strip.to_i
if (curr_age > oldest_age) then
oldest_name = curr_name
oldest_age = curr_age
end
end
end
puts "The oldest person is #{oldest_name} and he/she is #{oldest_age} years old."
Notice the use of String#strip when acquiring the age. According to the format of the file, this piece of data (the age) has a space before the first number and you need to strip this before converting it using String#to_i.
EDIT:
Since you may have more than one person with the maximum age in the list, you may do it in two passes:
oldest_age = 0
File.open('nameage.txt') do |f|
f.each_line do |line|
curr_age = line.split(":")[1].strip.to_i
if (curr_age > oldest_age) then
oldest_age = curr_age
end
end
end
oldest_people = Array.new
File.open('nameage.txt') do |f|
f.each_line do |line|
data = line.split(":")
curr_name = data[0]
curr_age = data[1].strip.to_i
oldest_people.push(curr_name) if (curr_age == oldest_age)
end
end
oldest_people.each { |person| p "#{person} is #{oldest_age}" }
I believe that now this will give you what you need.

Merging Ranges using Sets - Error - Stack level too deep (SystemStackError)

I have a number of ranges that I want merge together if they overlap. The way I’m currently doing this is by using Sets.
This is working. However, when I attempt the same code with a larger ranges as follows, I get a `stack level too deep (SystemStackError).
require 'set'
ranges = [Range.new(73, 856), Range.new(82, 1145), Range.new(116, 2914), Range.new(3203, 3241)]
set = Set.new
ranges.each { |r| set << r.to_set }
set.flatten!
sets_subsets = set.divide { |i, j| (i - j).abs == 1 } # this line causes the error
puts sets_subsets
The line that is failing is taken directly from the Ruby Set Documentation.
I would appreciate it if anyone could suggest a fix or an alternative that works for the above example
EDIT
I have put the full code I’m using here:
Basically it is used to add html tags to an amino acid sequence according to some features.
require 'set'
def calculate_formatting_classes(hsps, signalp)
merged_hsps = merge_ranges(hsps)
sp = format_signalp(merged_hsps, signalp)
hsp_class = (merged_hsps - sp[1]) - sp[0]
rank_format_positions(sp, hsp_class)
end
def merge_ranges(ranges)
set = Set.new
ranges.each { |r| set << r.to_set }
set.flatten
end
def format_signalp(merged_hsps, sp)
sp_class = sp - merged_hsps
sp_hsp_class = sp & merged_hsps # overlap regions between sp & merged_hsp
[sp_class, sp_hsp_class]
end
def rank_format_positions(sp, hsp_class)
results = []
results += sets_to_hash(sp[0], 'sp')
results += sets_to_hash(sp[1], 'sphsp')
results += sets_to_hash(hsp_class, 'hsp')
results.sort_by { |s| s[:pos] }
end
def sets_to_hash(set = nil, cl)
return nil if set.nil?
hashes = []
merged_set = set.divide { |i, j| (i - j).abs == 1 }
merged_set.each do |s|
hashes << { pos: s.min.to_i - 1, insert: "<span class=#{cl}>" }
hashes << { pos: s.max.to_i - 0.1, insert: '</span>' } # for ordering
end
hashes
end
working_hsp = [Range.new(7, 136), Range.new(143, 178)]
not_working_hsp = [Range.new(73, 856), Range.new(82, 1145),
Range.new(116, 2914), Range.new(3203, 3241)]
sp = Range.new(1, 20).to_set
# working
results = calculate_formatting_classes(working_hsp, sp)
# Not Working
# results = calculate_formatting_classes(not_working_hsp, sp)
puts results
Here is one way to do this:
ranges = [Range.new(73, 856), Range.new(82, 1145),
Range.new(116, 2914), Range.new(3203, 3241)]
ranges.size.times do
ranges = ranges.sort_by(&:begin)
t = ranges.each_cons(2).to_a
t.each do |r1, r2|
if (r2.cover? r1.begin) || (r2.cover? r1.end) ||
(r1.cover? r2.begin) || (r1.cover? r2.end)
ranges << Range.new([r1.begin, r2.begin].min, [r1.end, r2.end].max)
ranges.delete(r1)
ranges.delete(r2)
t.delete [r1,r2]
end
end
end
p ranges
#=> [73..2914, 3203..3241]
The other answers aren't bad, but I prefer a simple recursive approach:
def merge_ranges(*ranges)
range, *rest = ranges
return if range.nil?
# Find the index of the first range in `rest` that overlaps this one
other_idx = rest.find_index do |other|
range.cover?(other.begin) || other.cover?(range.begin)
end
if other_idx
# An overlapping range was found; remove it from `rest` and merge
# it with this one
other = rest.slice!(other_idx)
merged = ([range.begin, other.begin].min)..([range.end, other.end].max)
# Try again with the merged range and the remaining `rest`
merge_ranges(merged, *rest)
else
# No overlapping range was found; move on
[ range, *merge_ranges(*rest) ]
end
end
Note: This code assumes each range is ascending (e.g. 10..5 will break it).
Usage:
ranges = [ 73..856, 82..1145, 116..2914, 3203..3241 ]
p merge_ranges(*ranges)
# => [73..2914, 3203..3241]
ranges = [ 0..10, 5..20, 30..50, 45..80, 50..90, 100..101, 101..200 ]
p merge_ranges(*ranges)
# => [0..20, 30..90, 100..200]
I believe your resulting set has too many items (2881) to be used with divide, which if I understood correctly, would require 2881^2881 iterations, which is such a big number (8,7927981983090337174360463368808e+9966) that running it would take nearly forever even if you didn't get stack level too deep error.
Without using sets, you can use this code to merge the ranges:
module RangeMerger
def merge(range_b)
if cover?(range_b.first) && cover?(range_b.last)
self
elsif cover?(range_b.first)
self.class.new(first, range_b.last)
elsif cover?(range_b.last)
self.class.new(range_b.first, last)
else
nil # Unmergable
end
end
end
module ArrayRangePusher
def <<(item)
if item.kind_of?(Range)
item.extend RangeMerger
each_with_index do |own_item, idx|
own_item.extend RangeMerger
if new_range = own_item.merge(item)
self[idx] = new_range
return self
end
end
end
super
end
end
ranges = [Range.new(73, 856), Range.new(82, 1145), Range.new(116, 2914), Range.new(3203, 3241)]
new_ranges = Array.new
new_ranges.extend ArrayRangePusher
ranges.each do |range|
new_ranges << range
end
puts ranges.inspect
puts new_ranges.inspect
This will output:
[73..856, 82..1145, 116..2914, 3203..3241]
[73..2914, 3203..3241]
which I believe is the intended output for your original problem. It's a bit ugly, but I'm a bit rusty at the moment.
Edit: I don't think this has anything to do with your original problem before the edits which was about merging ranges.

Need help indenting tags in the output in Ruby

UPDATE: OK, so I implemented your code, but now the indentation is not showing up! Any ideas what might be wrong? I modified the code so that it would attempt to pass my original test (this is only an exercise so in real life I would not be overriding the XmlDocument class) and here is the modified code:
class XmlDocument
attr_reader :indent_depth, :bool
def initialize(bool = false, indent_depth = 0)
#indent_depth = indent_depth
#bool = bool
end
def method_missing(name, *args)
indentation = ' '*indent_depth
attrs = (args[0] || {}).map { |k, v| " #{k}='#{v}'" }.join(' ')
if block_given?
puts indent_depth
opening = "#{indentation}<#{name}#{attrs}>"
contents = yield(XmlDocument.new(true,indent_depth+1))
closing = "#{indentation}</#{name}>"
bool ? opening + "\n" + contents + "\n" + closing : opening + contents + closing
else
"#{indentation}<#{name}#{attrs}/>"
end
end
end
I'm trying to get the method to pass this test:
it "indents" do
#xml = XmlDocument.new(true)
#xml.hello do
#xml.goodbye do
#xml.come_back do
#xml.ok_fine(:be => "that_way")
end
end
end.should ==
"<hello>\n" +
" <goodbye>\n" +
" <come_back>\n" +
" <ok_fine be='that_way'/>\n" +
" </come_back>\n" +
" </goodbye>\n" +
"</hello>\n"
...but I'm unsure as to where to go with my code, below. I was thinking of using a counter to keep track of how far indented we have to go. I tried some code, but then deleted it because it was getting too messy and I have a feeling that the indentation should not be too complicated to implement.
class XmlDocument
def initialize(bool = false)
#bool = bool
end
def send(tag_name)
"<#{tag_name}/>"
end
def method_missing(meth, arg={}, &block)
arbitrary_method = meth.to_s
tag_string = ''
# 1) test for block
# 2) test for arguments
# 3) test for hash
if block_given? # check for #xml.hello do; #xml.goodbye; end
if yield.class == String # base case: #xml.hello do; "yellow"; end
"<#{arbitrary_method}>#{yield}</#{arbitrary_method}>"
else # in the block we do not have a string, we may have another method
method_missing(yield)
end
elsif arg.empty? # no arguments e.g. #xml.hello
send(arbitrary_method)
else # hash as argument e.g. #xml.hello(:name => 'dolly')
send("#{arbitrary_method} #{arg.keys[0]}='#{arg.values[0]}'")
end
end
end
Your code needs a lot of work - some pointers:
Do not override the send method!
Don't call yield over and over - you don't know what side effects you might cause, not to mention a performance hit - call it once, and remember the return value.
You might want to read up on how to write a DSL (here is a blogpost on the subject), to see how it was done correctly in other places.
Ignoring the above, I will try to answer your question regarding indentation.
In a DSL use case, you might want to use a context object which holds the indentation depth as state:
class Indented
attr_reader :indent_depth
def initialize(indent_depth = 0)
#indent_depth = indent_depth
end
def method_missing(name, *args)
indentation = ' ' * indent_depth
attrs = (args[0] || {}).map { |k, v| "#{k}='#{v}'" }.join(' ')
if block_given?
"#{indentation}<#{name} #{attrs}>\n" +
yield(Indented.new(indent_depth + 1)) +
"\n#{indentation}</#{name}>"
else
"#{indentation}<#{name} #{attrs}/>"
end
end
end
xml = Indented.new
puts xml.hello do |x|
x.goodbye do |x|
x.come_back do |x|
x.ok_fine(:be => "that_way")
end
end
end
# => <hello >
# => <goodbye >
# => <come_back >
# => <ok_fine be='that_way'/>
# => </come_back>
# => </goodbye>
# => </hello>

Sorting array with multiple variables

I am trying to create a program where by the user can enter multiple names. those names are then displayed under each other in alphabetical order, and print(display) every second name backwards. i have gone through several tutorials this is my second day using ruby.. here is what i have so far.
name_list = {}
puts 'please enter names seperated by a space:'
name_list = gets.chomp
names = name_list.split(" ")
to grab names...
names.sort do |a,b| a.upcase <=> b.upcase end
display = "#{names}"
for ss in 0...display.length
print ss, ": ", display[ss], "\n"
end
to arrange them alphabetically and under each other.
i am really struggling to mesh it all together i think i have got at least half a dozen errors in here...if i am on the wrong path could someone guide me to some info so i can start again??
EDIT
i also had this idea of using a class.
but i would have to program the names in i wanted the user to be able to add info via the consol.
class A
def initialize(name)
#name = name
end
def to_s
#name.reverse
end
end
>> a = [A.new("greg"),A.new("pete"),A.new("paul")]
>> puts a
Problems in your code:
name_list defined as an empty hash at the top but not used.
split(" ") -> split
sort { |a, b| a.method <=> b.method } -> sort_by { |x| x.method } -> sort_by(&:method)
sort is not an in-place operation, assign the result (or directly use it).
display = "#{names}" -> display = names
for ss in 0...display.length -> enumerable.each_with_index { |item, index| ... }
don't write do/end in one-liners, use { ... }
I'd write:
puts 'Please enter names separated by spaces'
gets.split.sort_by(&:upcase).each_with_index do |name, index|
puts "%s: %s" % [index, (index % 2).zero? ? name : name.reverse]
end
A few pointers then:
names.sort do |a,b| a.upcase <=> b.upcase end # Will not modify the "names" array, but will return a sorted array.
names.sort! do |a,b| a.upcase <=> b.upcase end # Will modify the "names" array.
To display your names:
names.each_with_index do |name, index|
if index % 2 == 0
puts name
else
puts name.reverse
end
end
puts 'please enter names seperated by a space`enter code here` :'
names = gets.chomp.split(" ")
names.sort! {|a,b| a.upcase <=> b.upcase } # For a single line use {..} instead of do..end
names.each_with_index do |n,i|
if i % 2 == 0
p n
else
p n.reverse
end
end
You can also use a ternary operator, I used the full if else block for readability in this case.
names.each_with_index do |n,i|
p (i % 2 == 0) ? n : n.reverse
end
EDIT
command = ""
names = []
while command != "exit"
puts 'please enter names seperated by a space`enter code here` :'
command = gets.chomp!
if command == "display"
names.sort! {|a,b| a.upcase <=> b.upcase } # For a single line use {..} instead of do..end
names.each_with_index do |n,i|
if i % 2 == 0
p n
else
p n.reverse
end
end
else
names << command
end
end

Recursively merge multidimensional arrays, hashes and symbols

I need a chunk of Ruby code to combine an array of contents like such:
[{:dim_location=>[{:dim_city=>:dim_state}]},
:dim_marital_status,
{:dim_location=>[:dim_zip, :dim_business]}]
into:
[{:dim_location => [:dim_business, {:dim_city=>:dim_state}, :dim_zip]},
:dim_marital_status]
It needs to support an arbitrary level of depth, though the depth will rarely be beyond 8 levels deep.
Revised after comment:
source = [{:dim_location=>[{:dim_city=>:dim_state}]}, :dim_marital_status, {:dim_location=>[:dim_zip, :dim_business]}]
expected = [{:dim_location => [:dim_business, {:dim_city=>:dim_state}, :dim_zip]}, :dim_marital_status]
source2 = [{:dim_location=>{:dim_city=>:dim_state}}, {:dim_location=>:dim_city}]
def merge_dim_locations(array)
return array unless array.is_a?(Array)
values = array.dup
dim_locations = values.select {|x| x.is_a?(Hash) && x.has_key?(:dim_location)}
old_index = values.index(dim_locations[0]) unless dim_locations.empty?
merged = dim_locations.inject({}) do |memo, obj|
values.delete(obj)
x = merge_dim_locations(obj[:dim_location])
if x.is_a?(Array)
memo[:dim_location] = (memo[:dim_location] || []) + x
else
memo[:dim_location] ||= []
memo[:dim_location] << x
end
memo
end
unless merged.empty?
values.insert(old_index, merged)
end
values
end
puts "source1:"
puts source.inspect
puts "result1:"
puts merge_dim_locations(source).inspect
puts "expected1:"
puts expected.inspect
puts "\nsource2:"
puts source2.inspect
puts "result2:"
puts merge_dim_locations(source2).inspect
I don't think there's enough detail in your question to give you a complete answer, but this might get you started:
class Hash
def recursive_merge!(other)
other.keys.each do |k|
if self[k].is_a?(Array) && other[k].is_a?(Array)
self[k] += other[k]
elsif self[k].is_a?(Hash) && other[k].is_a?(Hash)
self[k].recursive_merge!(other[k])
else
self[k] = other[k]
end
end
self
end
end

Resources