How to change step value inside the loop? - ruby

I want to increase my step value in each loop iteration, but my solution is not working.
n=1
(0..100).step(n) do |x|
puts x
n+=1
end
Is there any way to change "n" or I'm must using "while loop" or smth else?

I assuming you are trying to print 1, 3, 6, 10, 15, 21 etc.
step documentation says:
Iterates over range, passing each nth element to the block. If the range
contains numbers, n is added for each iteration.
So what you are trying to do can't be done with step. A while or traditional for loop should do the trick.

Here's a custom Enumerator based on Aurélien Bottazini's answer:
tri = Enumerator.new do |y|
n = 0
step = 1
loop do
y << n
n = n + step
step += 1
end
end
tri.take(10)
#=> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45]
tri.take_while { |i| i < 100 }
#=> [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 66, 78, 91]

I think the best way to do what you want is to use a while loop.
step = 1
last = 100
i = 0
while (i < last )
puts i
step += 1
i += step
end
It might be possible to do it with step, but it requires you to fully understand how it works, and maybe using some hacky stuff to make it work. But why to do that when you have a simple solution already available to you?

I would do it with loop and a break
n = 0
step = 1
loop do
puts n
n = n + step
step += 1
break if n > 100
end

What I think you are asking, now that I look at it again, is changing the argument given to step inside the block given to the step method. This is not possible, due to the argument for step being evaluated before step can continue on its job of running the given block, and assigning the block variable x.

Related

Ruby's times method and for loop does the same thing, but what are the respective processes?

I have a paradigm where the purpose is to calculate "the total accumulated from 'variable from' to 'variable to'". I got two methods, but I don't know exactly what steps they go through.
The first is the times method. I don't understand what (to - from +1) produces and why (i + from)? Also, this code throws syntax error, unexpected end, expecting end-of-input, I don't know where I am going wrong .
from = 10
to = 20
sum = 0
(to - from +1).times to |i|
sum = sum + (i + from)
end
puts sum
Then there is the for loop. I can't understand what is the "total sum accumulated from 'variable from' to 'variable to'", what is the process of adding it?
from = 10
to = 20
sum = 0
for i in from..to
sum = sum + i
end
puts sum
I'm bad at math, thank you all.
Using the IRB you can create an array that represents all the numbers from to to:
irb(main):043:0> from=10
=> 10
irb(main):044:0> to=20
=> 20
irb(main):047:0> a=(from..to).to_a
=> [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
You can get a sum of that array:
irb(main):048:0> a.sum
=> 165
And, actually, you don't need to make an array at all:
irb(main):049:0> (from..to).sum
=> 165
Side note. If you have three dots the last value of to is not included and that is a different sum obviously:
irb(main):053:0> (from...to).to_a
=> [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
irb(main):054:0> (from...to).sum
=> 145
So upto should really be called uptoandincluding:
irb(main):055:0> from.upto(to).to_a
=> [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
You can also use reduce or the alias inject either with a block or symbol to an operation to reduct a list the the pairwise application of the operator.
Example (reduce and inject are the same, but I usually use inject with a block or reduce with the parenthesis):
# block form where n is repeatedly added to an accumulator
irb(main):058:0> (from..to).inject{|sum, n| sum+n}
=> 165
# the operator + is repeated applied from the left to right
irb(main):059:0> (from..to).reduce(:+)
=> 165
Here are a few idiomatic ways of solving this problem in Ruby. They might be a little easier to follow than the solution you proposed.
sum = 0
10.upto(20) do |i|
sum = sum + i
end
For the first iteration of loop, i will = 10, the second, i will = 11 and so "up to" 20. Each time it will add the value of i to sum. When it completes, sum will have a value of 165.
Another way is this:
10.upto(20).sum
The reason this works is that 10.upto(20) returns an Enumerator. You can call sum on an Enumerator.

Detect outlier in repeating sequence

I have a repeating sequence of say 0~9 (but may start and stop at any of these numbers). e.g.:
3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2
And it has outliers at random location, including 1st and last one, e.g.:
9,4,5,6,7,8,9,0,1,2,3,4,8,6,7,0,9,0,1,2,3,4,1,6,7,8,9,0,1,6
I need to find & correct the outliers, in the above example, I need correct the first "9" into "3", and "8" into "5", etc..
What I came up with is to construct a sequence with no outlier of desired length, but since I don't know which number the sequence starts with, I'd have to construct 10 sequences each starting from "0", "1", "2" ... "9". And then I can compare these 10 sequences with the given sequence and find the one sequence that match the given sequence the most. However this is very inefficient when the repeating pattern gets large (say if the repeating pattern is 0~99, I'd need to create 100 sequences to compare).
Assuming there won't be consecutive outliers, is there a way to find & correct these outliers efficiently?
edit: added some explanation and added the algorithm tag. Hopefully it is more appropriate now.
I'm going to propose a variation of #trincot's fine answer. Like that one, it doesn't care how many outliers there may be in a row, but unlike that one doesn't care either about how many in a row aren't outliers.
The base idea is just to let each sequence element "vote" on what the first sequence element "should be". Whichever gets the most votes wins. By construction, this maximizes the number of elements left unchanged: after the 1-liner loop ends, votes[i] is the number of elements left unchanged if i is picked as the starting point.
def correct(numbers, mod=None):
# this part copied from #trincot's program
if mod is None: # if argument is not provided:
# Make a guess what the range is of the values
mod = max(numbers) + 1
votes = [0] * mod
for i, x in enumerate(numbers):
# which initial number would make x correct?
votes[(x - i) % mod] += 1
winning_count = max(votes)
winning_numbers = [i for i, v in enumerate(votes)
if v == winning_count]
if len(winning_numbers) > 1:
raise ValueError("ambiguous!", winning_numbers)
winning_number = winning_numbers[0]
for i in range(len(numbers)):
numbers[i] = (winning_number + i) % mod
return numbers
Then, e.g.,
>>> correct([9,4,5,6,7,8,9,0,1,2,3,4,8,6,7,0,9,0,1,2,3,4,1,6,7,8,9,0,1,6])
[3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2]
but
>>> correct([1, 5, 3, 7, 5, 9])
...
ValueError: ('ambiguous!', [1, 4])
That is, it's impossible to guess whether you want [1, 2, 3, 4, 5, 6] or [4, 5, 6, 7, 8, 9]. They both have 3 numbers "right", and despite that there are never two adjacent outliers in either case.
I would do a first scan of the list to find the longest sublist in the input that maintains the right order. We will then assume that those values are all correct, and calculate backwards what the first value would have to be to produce those values in that sublist.
Here is how that would look in Python:
def correct(numbers, mod=None):
if mod is None: # if argument is not provided:
# Make a guess what the range is of the values
mod = max(numbers) + 1
# Find the longest slice in the list that maintains order
start = 0
longeststart = 0
longest = 1
expected = -1
for last in range(len(numbers)):
if numbers[last] != expected:
start = last
elif last - start >= longest:
longest = last - start + 1
longeststart = start
expected = (numbers[last] + 1) % mod
# Get from that longest slice what the starting value should be
val = (numbers[longeststart] - longeststart) % mod
# Repopulate the list starting from that value
for i in range(len(numbers)):
numbers[i] = val
val = (val + 1) % mod
# demo use
numbers = [9,4,5,6,7,8,9,0,1,2,3,4,8,6,7,0,9,0,1,2,3,4,1,6,7,8,9,0,1,6]
correct(numbers, 10) # for 0..9 provide 10 as argument, ...etc
print(numbers)
The advantage of this method is that it would even give a good result if there were errors with two consecutive values, provided that there are enough correct values in the list of course.
Still this runs in linear time.
Here is another way using groupby and count from Python's itertools module:
from itertools import count, groupby
def correct(lst):
groupped = [list(v) for _, v in groupby(lst, lambda a, b=count(): a - next(b))]
# Check if all groups are singletons
if all(len(k) == 1 for k in groupped):
raise ValueError('All groups are singletons!')
for k, v in zip(groupped, groupped[1:]):
if len(k) < 2:
out = v[0] - 1
if out >= 0:
yield out
else:
yield from k
else:
yield from k
# check last element of the groupped list
if len(v) < 2:
yield k[-1] + 1
else:
yield from v
lst = "9,4,5,6,7,8,9,0,1,2,3,4,8,6,7,0,9,0,1,2,3,4,1,6,7,8,9,0,1,6"
lst = [int(k) for k in lst.split(',')]
out = list(correct(lst))
print(out)
Output:
[3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2]
Edit:
For the case of [1, 5, 3, 7, 5, 9] this solution will return something not accurate, because i can't see which value you want to modify. This is why the best solution is to check & raise a ValueError if all groups are singletons.
Like this?
numbers = [9,4,5,6,7,8,9,0,1,2,3,4,8,6,7,0,9,0,1,2,3,4,1,6,7,8,9,0,1,6]
i = 0
for n in numbers[:-1]:
i += 1
if n > numbers[i] and n > 0:
numbers[i-1] = numbers[i]-1
elif n > numbers[i] and n == 0:
numbers[i - 1] = 9
n = numbers[-1]
if n > numbers[0] and n > 0:
numbers[-1] = numbers[0] - 1
elif n > numbers[0] and n == 0:
numbers[-1] = 9
print(numbers)

How to limit each `do` loop by a different variable

I'm working on a bot. I have ships, each put in an array my_ships. Each ship is given an id when it is created independent of who created it, and each ship can be destroyed. This is the first array:
ship = Ship.new(
player_id, id,
Float(x),
Float(y),
Integer(hp),
Integer(status),
Integer(progress),
Integer(planet)
)
Each iteration sends commands. I run into timeout issues. I only have enough time to run ~100.
How do I limit my each loop to run only 100 times?
my_ships(0, 100).each do |ship|
gets me less ships to use as some are destroyed, and they are ordered by their id.
Assuming this isn't in some sort of database, where you should use a database query to select and limit (since nothing db-related is tagged), you can make use of Enumerable#lazy (this is a method on array's as well, since array's are Enumerable). You'll first want to select only the ships that are not destroyed and then take only the first 100 of those:
my_ships.lazy.select do |ship|
# Some logic to see if a ship is allowed to be iterated
end.first(100).each do |ship|
# your loop that runs commands
end
if it makes more sense, you can use reject instead of select:
my_ships.lazy.reject do |ship|
# Some logic to see if a ship should not be iterated
end.first(100).each do |ship|
# your loop that runs commands
end
to see a little clearer what this will do for you, consider the following example:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers.lazy.select do |number|
puts "running check for #{number}"
number.even?
end.first(2).each do |number|
puts "doing something with #{number}"
end
# running check for 1
# running check for 2
# running check for 3
# running check for 4
# doing something with 2
# doing something with 4
So in this example, I want to run a loop for the first 2 even numbers...if I just take the first 2 numbers, I get 1 even and 1 odd; I also don't want to loop through the entire list, because the check for is this even? might be expensive (it's not, but your check could be), or your list could be large and you only need a few items. This loops through just enough to get me the first 2 numbers that match my criteria and then let's me run my loop on them.
As suggested in the comments by #user3309314 you may wish to use next.
arr = (1..100).to_a
enum = arr.to_enum
hun = loop.with_object [] do |_,o|
if o.size == 10
break o
elsif enum.peek.even?
o << enum.next
else
enum.next
end
end
hun #=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
This loop goes through each element of arr via enumeration and adds it to another array o if it satisfies the condition even?. The loop breaks when o.size == 10.
Might be easier to create a more explicit enumerator then take from that. That way you can vary how many elements you need enum.take(8) gets you the first 8 elements etc.
enum = Enumerator.new do |y|
arr = (1..100).to_a
enum = arr.to_enum
loop { enum.peek.even? ? y << enum.next : enum.next }
end
enum.take 10 #=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

Is there way to assign and break within the block?

I know I can write it this way successfully:
def test_find_first_multiple_of_3
numbers = [2, 8, 9, 27, 24, 5]
found = nil
numbers.each do |number|
if number % 3 == 0
found = number
break
end
end
assert_equal 9, found
end
Is there anyway to do within the block? What am I missing? Or is just not possible?
numbers.each { |n| n % 3 == 0 ? (found = n then break) : nil }
def test_find_first_multiple_of_3
numbers = [2, 8, 9, 27, 24, 5]
found = nil
numbers.each { |n| n % 3 == 0 ? (found = n then break) : nil }
assert_equal 9, found
end
As pointed by other answers, there are other ruby ways to accomplish your algorithm goal, like using the .find method:
found = numbers.find { |n| (n % 3).zero? }
This way, you don't need to break your loop.
But, specifically answering your question, there are some ways to break the loop in the same line, if you want so:
use ; (multiple statements separator):
numbers.each { |n| n % 3 == 0 ? (found = n; break) : nil }
or put your assigment after break, that works too:
numbers.each { |n| n % 3 == 0 ? (break found = n) : nil }
I just used your code in the example, but, again, that's not a good pratice, because, as well pointed by #the Tin Man, "hurts readability and maintenance".
Also, as pointed by #akuhn, you don't need to use ternary here. You can simply use:
numbers.each { |n| break found = n if n % 3 == 0 }
** EDITED to include suggestions from #the Tin Man, #akuhn and #Eric Duminil, in order to warn OP that there are other alternatives to run his task, that doesn't need to break loop. The original answer was written just to answer OP's question specifically (one line break loop), without the code structure concern.
With common Ruby idioms your can write:
def test_find_first_multiple_of_3
numbers = [2, 8, 9, 27, 24, 5]
found = numbers.find { |n| (n % 3).zero? }
assert_equal 9, found
end
Yes, both break and next take an argument.
For your example though, best use find
founds = numbers.find { |n| n % 3 == 0 }
Generally in Ruby there is rarely a reason to break out of a loop.
You can typically use find or any of the other functions provided by the Enumerable module, like take_while and drop_while…
You can use the enumerable method find to find the first item that matches. Usually you will want to use enumerable methods like cycle, detect, each, reject, and others to make the code more compact while remaining understandable:
def test_find_first_multiple_of_3
numbers = [2, 8, 9, 27, 24, 5]
found = numbers.find { |number| number % 3 == 0 }
assert_equal 9, found
end

Splitting a string of numbers with different digit sizes in Ruby

I'm trying to figure out if there's a way to split a string that contains numbers with different digit sizes without having to use if/else statements. Is there an outright method for doing so. Here is an example string:
"123456789101112131415161718192021222324252627282930"
So that it would be split into an array containing 1-9 and 10-30 without having to first split the array into single digits, separate it, find the 9, and iterate through combining every 2 elements after the 9.
Here is the current way I would go about doing this to clarify:
single_digits, double_digits = [], []
string = "123456789101112131415161718192021222324252627282930".split('')
single_digits << string.slice!(0,9)
single_digits.map! {|e| e.to_i}
string.each_slice(2) {|num| double_digits << num.join.to_i}
This would give me:
single_digits = [1,2,3,4,5,6,7,8,9]
double_digits = [10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
As long as you can be sure that every number is greater than its predecessor and greater than zero, and every length of number from a single digit to the maximum is represented at least once, you could write this
def split_numbers(str)
numbers = []
current = 0
str.each_char do |ch|
current = current * 10 + ch.to_i
if numbers.empty? or current > numbers.last
numbers << current
current = 0
end
end
numbers << current if current > 0
numbers
end
p split_numbers('123456789101112131415161718192021222324252627282930')
output
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
For Anon's example of 192837453572 we get
[1, 9, 28, 37, 45, 357, 2]
Go through each character of the string, collecting single 'digits', until you find a 9 (set a controlling value and increment it by 1), then continue on collecting two digits, until you find 2 consecutive 9's, and continue on.
This can then be written to handle any sequence of numbers such as your example string.
You could do this:
str = "123456789101112131415161718192021222324252627282930"
result = str[0..8].split('').map {|e| e.to_i }
result += str[9..-1].scan(/../).map {|e| e.to_i }
It's essentially the same solution as yours, but slightly cleaner (no need to combine the pairs of digits). But yeah, if you want a generalizable solution to an arbitrary length string (including more than just 2 digits), that's a different question than what you seem to be asking.
UPDATE:
Well, I haven't been able to get this question out of my mind, because it seems like there could be a simple, generalizable solution. So here's my attempt. The basic idea is to keep a counter so that you know how many digits the number you want to slice out of the string is.
str = "123456789101112131415161718192021222324252627282930"
result = []
i = 1
done = str.length < 1
str_copy = str
while !done do
result << str_copy.slice!(0..i.to_s.size-1).to_i
done = true if str_copy.size == 0
i += 1
end
puts result
This generates the desired output, and is generalizable to a string of consecutive positive integers starting with 1. I'd be very interested to see other people's improvements to this -- it's not super succinct

Resources