This question already has answers here:
Count iteration on the Enumerable cycle
(5 answers)
Closed 6 years ago.
I have a set of objects that needs to repeat an indeterminate number of times. This would be easy enough to set up if the array were fixed:
>> enum = ['Start', 'Peak', 'Finish'].cycle
>> enum.first(7)
=> ['Start', 'Peak', 'Finish', 'Start', 'Peak', 'Finish', 'Start']
But the result I want is this:
>> enum = <Enumerator magic here>
>> enum.first(7)
=> ['Start Lap 1', 'Peak Lap 1', 'Finish Lap 1', 'Start Lap 2', 'Peak Lap 2', 'Finish Lap 2', 'Start Lap 3']
It seems like I should be able to start with (1..Float::INFINITY) and get the right result with #map or #each, but I'm having no luck. I know I could use (1..arbitrary_big_number) and make a big array, but hoping for a lazy-evaluated solution.
Thanks in advance.
Try this one
enum = Enumerator.new do |y|
lap = 1
ss = %w(Start Peak Finish).cycle
loop do
3.times { y << "#{ss.next} Lap #{lap}" }
lap += 1
end
end
enum.first(7)
=> ["Start Lap 1", "Peak Lap 1", "Finish Lap 1", "Start Lap 2", "Peak Lap 2", "Finish Lap 2", "Start Lap 3"]
Related
I have the following Ruby code, which calls the same function multiple times with different arguments, and pushes the results into a common array.
people_relations = []
people.zip(people_addresses).map do |person, address|
people_relations.push(createRelation(person, address))
end
people.zip(people_ph_numbers).map do |person, phone_number|
people_relations.push(createRelation(person, phone_number))
end
people.zip(people_aliases).map do |person, _alias|
people_relations.push(createRelation(person, _alias))
end
def createRelation(e1, e2)
[true, false].sample ? CurrentRelation.new(e1, e2) : PastRelation.new(e1, e2)
end
This code works just fine, but I feel like this is not the idiomatic Ruby way of doing things, and can be improved by compressing the code into less lines or made to look cleaner.
Is there a better way to write the code that appears above?
You could create an array that contains all the people "attributes" you're going to use, and with Enumerable#each_with_object you can assign an initial array to fill with the result of each call to createRelation():
attributes = [people_addresses, people_ph_numbers, people_aliases]
relations = people.each_with_object([]).with_index do |(person, memo), index|
attributes.each do |attribute|
memo << createRelation(person, attribute[index])
end
end
I'd probably go with a transpose -> flat_map solution for this myself, for instance given:
def CreateRelation(person, relationship)
if [true, false].sample
"#{person} is currently related to #{relationship}"
else
"#{person} used to be related to #{relationship}"
end
end
people = ['Person 1', 'Person 2', 'Person 3']
addresses = ['Person 1 Address', 'Person 2 Address', 'Person 3 Address']
phone_numbers = ['Person 1 Phone', 'Person 2 Phone', 'Person 3 Phone']
aliases = ['Person 1 AKA', 'Person 2 AKA', 'Person 3 AKA']
We can stick those 4 arrays into a single array and then transpose them, so the first element of each ends up in an array with each other, the second in another, and the last in a third:
[people, addresses, phone_numbers, aliases].transpose # => [
# ["Person 1", "Person 1 Address", "Person 1 Phone", "Person 1 AKA"],
# ["Person 2", "Person 2 Address", "Person 2 Phone", "Person 2 AKA"],
# ["Person 3", "Person 3 Address", "Person 3 Phone", "Person 3 AKA"]]
and then you can flat_map those by calling CreateRelation:
result = [people, addresses, phone_numbers, aliases].transpose.flat_map do |person, *relations|
relations.map { |relationship| CreateRelation(person, relationship) }
end
#["Person 1 used to be related to Person 1 Address",
# "Person 1 used to be related to Person 1 Phone",
# "Person 1 used to be related to Person 1 AKA",
# "Person 2 is currently related to Person 2 Address",
# "Person 2 used to be related to Person 2 Phone",
# "Person 2 is currently related to Person 2 AKA",
# "Person 3 is currently related to Person 3 Address",
# "Person 3 used to be related to Person 3 Phone",
# "Person 3 used to be related to Person 3 AKA"]
Or, at that point you could stick with just iterating and pushing, if you don't want to map/flat_map.
The more I think about it, the more I think I'd go with transpose -> each_with_object, instead of flat_map...less "create an array and then throw it away", I'll leave this with flat_map though because it is another option and #Sebastian Palma has each_with_object covered.
class Test
def self.take_test( question, options, answer )
puts question
options.each_with_index { |option, idx| puts "#{ idx + 1 }: #{ option}" }
print "Answer: "
reply = gets.to_i
if answer == reply
puts "Correct!"
else
puts "Wrong. The answer is: " + answer.to_s
end
end
end
file = File.open("Matematik.txt", "r")
This is what i tried to do:
IO.foreach("Matematik.txt") { |line| Test.take_test(line) }
This is how the questions are set up in the file:
'What is 2+2?', [ '2', '3', '4', '5', ], 4
'What is 3+3?', [ '3', '6', '9', ], 6
I get the error: take_test wrong number of arguments given (given 1, expected 3) (ArgumentError)
It seems like it reads the line like 1 argument. Is there a way to read the lines exactly as i stands, and input it like this?:
#Test.take_test('What is 2+2?', [ '2', '3', '4', '5', ], 4)
In theory, yes, with eval("Test.take_test(#{line})"). However, eval is evil, and to be avoided if at all possible.
It would be much easier if you changed your file format so you can deconstruct it easily. (It is not impossible with your format, it's just you have a lot of unnecessary work, when compared to a simpler format.) For example, given lines formatted like CSV:
"What is 2+2?",2,3,4,5,4
it is very easy to do the following:
require 'csv'
Question = Struct.new(:text, :options, :answer)
questions = CSV.read("Mathematik.csv").map { |text, *options, answer|
Question.new(text, options, answer.to_i)
}
questions[0].text
# => "What is 3+3?"
questions[0].options
# => ["2", "3", "4", "5"]
questions[0].answer
# => 4
I have an array that looks like this:
array = [
"timestamp 1",
"data 1",
"data 2",
"data 3",
"timestamp 2",
"data "1",
"timestamp 3",
".."
]
etc
I want to loop through my array, and turn it into a hash data structure that looks like:
hash = {
"timestamp 1" => [ "data 1", " data 2", "data 3" ],
"timestamp 2" => [ "data 1" ],
}
I can't figure out a good "rubyish" way of doing it. I'm looping through the array, and I just quite can't seem to figure out how to keep track of where I am at, and assign to the hash as needed.
# Let's comb through the array, and map the time value to the subsequent lines beneath
array.each do |e|
if timestamp?(e)
hash["#{e}"] == nil
else
# last time stamp here => e
end
EDIT: Here is the timestamp? method
def timestamp?(string)
begin
return true if string =~ /[a-zA-z][a-z][a-z]\s[a-zA-z][a-z][a-z]\s\d\d\s\d\d:\d\d:\d\d\s\d\d\d\d/
false
rescue => msg
puts "Error in timestamp? => #{msg}"
exit
end
end
array = [
"timestamp 1",
"data 1",
"data 2",
"data 3",
"timestamp 2",
"data 1",
"timestamp 3",
"data 2"
]
hsh = {}
ary = []
array.each do |line|
if line.start_with?("timestamp")
ary = Array.new
hsh[line] = ary
else
ary << line
end
end
puts hsh.inspect
I would do as below:
array = [
"timestamp 1",
"data 1",
"data 2",
"data 3",
"timestamp 2",
"data 1",
]
Hash[array.slice_before{|i| i.include? 'timestamp'}.map{|a| [a.first,a[1..-1]]}]
# => {"timestamp 1"=>["data 1", "data 2", "data 3"], "timestamp 2"=>["data 1"]}
Hash[array.slice_before{|e| e.start_with?("timestamp ")}.map{|k, *v| [k, v]}]
Output
{
"timestamp 1" => [
"data 1",
"data 2",
"data 3"
],
"timestamp 2" => ["data 1"],
"timestamp 3" => [".."]
}
You can keep track of the last hash key using an outside variable. It will be persisted across all iterations:
h = {}
last_group = nil
array.each do |e|
if timestamp?(e)
array[e] = []
last_group = e
else
h[last_group] << e
end
end
last_timestamp = nil
array.reduce(Hash.new(){|hsh,k| hsh[k]=[]}) do |hsh, m|
if m =~ /timestamp/
last_timestamp = m
else
hsh[last_timestamp] << m
end
hsh
end
hash = (Hash.new { |this, key| this[key] = [] } ).tap do |hash|
current_timestamp = nil
array.each do |element|
current_timestamp = element if timestamp? element
hash[current_timestamp] << element unless timestamp? element
end
end
Using an outside variable to keep track of the current timestamp, but wrapping it in a closure to avoid polluting the namespace.
I know this has already been answered, but there are so many ways to do this.
I prefer these two ways, they might not be fast but i find them readable:
my_hash = Hash.new
array.slice_before(/timestamp/).each do |array|
key, *values = array
my_hash[key] = values
end
or
one_liner = Hash[array.slice_before(/timestamp/).map{|x|[x.shift, x]}]
Just wondering, is there a way to use macros in Ruby that does an in-text substitution the way C would work?
For example:
define ARGS 1,2
sum(ARGS) # returns 3
EDIT:
More specifically my problem looks more like:
#button1 = FXButton.new(self, "Button 1",:opts => BUTTONPROPERTIES,:width => width, :height => height)
#button2 = FXButton.new(self, "Button 2",:opts => BUTTONPROPERTIES,:width => width, :height => height)
#button3 = FXButton.new(self, "Button 3",:opts => BUTTONPROPERTIES,:width => width, :height => height)
And ideally I'd want the code to look like:
#button1 = FXButton.new(self, "Button 1", ALLBUTTONPROPERTIES)
#button2 = FXButton.new(self, "Button 2", ALLBUTTONPROPERTIES)
#button3 = FXButton.new(self, "Button 3", ALLBUTTONPROPERTIES)
Notice how I have "width" and "height" variables that won't properly be passed to the initialization of the FXButton class if I just set them to some predetermined value. Is there some kind of code substitution that would take care of this issue?
You don't need a macro. Just define a variable or a constant.
A = [1, 2]
A.inject(:+) # => 3
After the edit to your question
You can do like this:
ALLBUTTONPROPERTIES = ->{{opts: => BUTTONPROPERTIES, width: width, height: height}}
and within a context where the constants and variables BUTTONPROPERTIES, width, height are assigned some value, do this:
#button1 = FXButton.new(self, "Button 1", ALLBUTTONPROPERTIES.call)
#button2 = FXButton.new(self, "Button 2", ALLBUTTONPROPERTIES.call)
#button3 = FXButton.new(self, "Button 3", ALLBUTTONPROPERTIES.call)
There's probably another way around what you are trying to do. Preprocessors macros doesn't make sense because Ruby is not a compiled language, it is an interpreted language.
Particularly for your example there's a very clean way to do that:
args = [1, 2]
sum(*args) # equivalent to sum( 1, 2 )
There is no preprocess in ruby, a macros does not make sense. Simply use string constant or whatever other type of constant you need.
Ruby way of solving your problem would probably be a bit different from your approach:
#buttons = ["Button 1", "Button 2", "Button 3"].map do |name|
FXButton.new(self, name,
:opts => BUTTONPROPERTIES,
:width => width,
:height => height)
end
In this example you don't have #button1, #button2, #button3 variables. Instead #buttons is array containing all three.
I am trying to learn Ruby, and arrays are giving me some trouble.
I have input that I flatten down to the pattern "name, number, name, number". I then want to make an array of 2-element arrays, each containing a name and the next number.
When I push these 2-element arrays into another array the seem to automatically flatten to a 0-dimensional array. What I want is the final array to be of size [N/2][2], N being number of names, or numbers in the input.
http://pastie.org/3542269
The puts with the comment does not happen until all of the elements from the pairs array has been printed, so it looks like this:
Name
1
Name
2
Name
3
When I expected this:
Name
1
Name
2
Name
3
I guess my questions are:
How do I put arrays inside an array, to make a jagged one?
How do I keep track of how many dimensions my arrays are in Ruby? It's so much easier when you have to declare a size.
some_array = [[["Name 1", "value 1"], ["Name 2", "value 2"]], [["Name 3", "value 3"], ["Name 4", "value 4"]]]
array = some_array.flatten
new_array = array.each_slice(2).map do |a, b|
[a,b]
end
#=> [["Name 1", "value 1"],
#=> ["Name 2", "value 2"],
#=> ["Name 3", "value 3"],
#=> ["Name 4", "value 4"]]
which is similar to some_array.flatten(1)