I have an application whose purpose is to take in coordinates from the command line and output directions from 0,0 to each coordinate eg 0,0 to 1,1 would be EN as in East North. When application reaches that coordinate it would output D as in drop delivery. So if I enter:
“ ./testapplication.rb "5x5 (2, 2) (3, 5)"
The output is:
EENNDENNND
So far I have the following:
#!/home/eamonn/.rbenv/shims/ruby
instructions = ''
addresses = ARGV.to_s.scan(/\(([^\)]+)\)/)
starting_point = ['0, 0']
addresses.each do |point|
starting_point = starting_point[0].split(", ")
destination = point[0].split(", ")
x = destination[0].to_i - starting_point[0].to_i
y = destination[1].to_i - starting_point[1].to_i
if x < 0
instructions << 'W' * x.abs
elsif x > 0
instructions << 'E' * x
end
if y < 0
instructions << 'S' * y.abs
elsif y > 0
instructions << 'N' * y
end
instructions << "D"
starting_point = point
end
puts instructions
While the application works I feel there are a few problems with it such as the efficiency of the code so any pointers are appreciated.
Also I am used to writing ruby on rails applications but as I am writing this as a standalone ruby application I am a bit confused as to how I would run tests for this. I have been looking into using rspec and creating a spec folder and writing tests in there. The testing approach I am considering is:
describe 'testing that application' do
it 'should return the right directions' do
expect(navigate(["5x5 (2, 2) (3, 5)"])).to equal('EENNDENNND')
end
end
Any advice as to whether I should include testing for incorrect input here or just perform some error handling when ARGV is passed into addresses.
You can refactor your code like this.
def generate_instructions(input)
addresses = input.to_s.scan(/\(([^\)]+)\)/)
instructions = ''
# use like array
starting_point = [0, 0]
addresses.each do |point|
sx, sy = starting_point # will set 1. param like first value
arr = point[0].split(", ") # split by , and set inside array
dx, dy = arr[0].to_i, arr[1].to_i # set array inside variables and re-type to -integer
x = dx - sx
y = dy - sy
# add into instructions
instructions << (x < 0 ? 'W' * x.abs : 'E' * x)
instructions << (y < 0 ? 'S' * y.abs : 'N' * y)
instructions << 'D'
# reset points to destination (use it like array)
starting_point = [dx, dy]
end
instructions
end
puts generate_instructions(ARGV) if ARGV
For testing use RSpec
require './testapplication.rb'
describe 'Test output for generate_instructions' do
it 'return EENNDENNND' do
expect(generate_instructions(["5x5 (2, 2) (3, 5)"])).to be == 'EENNDENNND'
end
end
I hope this helps.
There are improvements that can be made, but just to address the subject of testing, here's one approach you could take. Refactor your code to put the process in a single method, like so...
#!/home/eamonn/.rbenv/shims/ruby
def navigate(input)
instructions = ''
addresses = input.to_s.scan(/\(([^\)]+)\)/)
starting_point = ['0, 0']
addresses.each do |point|
starting_point = starting_point[0].split(", ")
destination = point[0].split(", ")
x = destination[0].to_i - starting_point[0].to_i
y = destination[1].to_i - starting_point[1].to_i
if x < 0
instructions << 'W' * x.abs
elsif x > 0
instructions << 'E' * x
end
if y < 0
instructions << 'S' * y.abs
elsif y > 0
instructions << 'N' * y
end
instructions << "D"
starting_point = point
end
instructions
end
if $0 == __FILE__
puts navigate(ARGV)
end
The conditional at the bottom means you will only really take the ARGV values if you're running the script standalone. If it's included in a test script, then we don't.
to test it, you need...
spec/testapplication_spec.rb
require './testapplication.rb'
describe 'testing the navigate method' do
it 'should return the right value' do
expect(navigate(["5x5 (2, 2) (3, 5)"]).to eq('EENNDENNND')
end
end
So in the test you mimic the ARGV input that the navigate method would receive, to see if it returns the correct result.
Here is a more Ruby-like way of making that calculation.
str = ' ./testapplication.rb "5x5 (2, 2) (3, 5) (1, 2)"'
r = /
\( # match a left paren
(\d+) # match one or more digits in capture group 1
,[ ]+ # match a comma followed by one or more spaces
(\d+) # match one or more digits in capture group 2
\) # match a right paren
/x # free-spacing regex definition mode
(conventionally written /\((\d+), +(\d+)\\)/)1.
str.scan(r).
map { |pair| pair.map(&:to_i) }.
unshift([0,0]).
each_cons(2).
map do |(fx,fy), (tx,ty)|
ew = tx-fx
ns = ty-fy
"%s%sD" % [ew >= 0 ? 'E'*ew : 'W'*(-ew), ns > 0 ? 'N'*ns : 'S'*(-ns)]
end.join
#=> "EENNDENNNDWWSSSD"
The steps are as follows2.
a = str.scan(/\((\d+), +(\d+)\)/)
#=> [["2", "2"], ["3", "5"], ["1", "2"]
b = a.map { |pair| pair.map(&:to_i) }
#=> [[2, 2], [3, 5], [1, 2]]
c = b.unshift([0,0])
#=> [[0, 0], [2, 2], [3, 5], [1, 2]]
d = c.each_cons(2)
#=> #<Enumerator: [[0, 0], [2, 2], [3, 5], [1, 2]]:each_cons(2)>
We can see the elements that will be generated by the enumerator d by converting it to an array.
d.to_a
#=> [[[0, 0], [2, 2]], [[2, 2], [3, 5]], [[3, 5], [1, 2]]]
Continuing,
e = d.map do |(fx,fy), (tx,ty)|
ew = tx-fx
ns = ty-fy
"%s%sD" % [ew >= 0 ? 'E'*ew : 'W'*(-ew), ns > 0 ? 'N'*ns : 'S'*(-ns)]
end
#=> ["EENND", "ENNND", "WWSSSD"]
and lastly,
e.join
#=> "EENNDENNNDWWSSSD"
Consider the first element generated by the enumerator d and passed to the block. The block variables are assigned values using disambiguation (aka decomposition) and the block calculation is performed3.
(fx,fy), (tx,ty) = d.next
#=> [[0, 0], [2, 2]]
fx
#=> 0
fy
#=> 0
tx
#=> 2
ty
#=> 2
ew = tx-fx
#=> 2
ns = ty-fy
#=> 2
"%s%sD" % [ew >= 0 ? 'E'*ew : 'W'*(-ew), ns > 0 ? 'N'*ns : 'S'*(-ns)]
#-> "%s%sD" % [true ? 'E'*ew : 'W'*(-ew), true ? 'N'*ns : 'S'*(-ns)]
#-> "%s%sD" % ['E'*ew, 'N'*ns]
#=> "EENND"
The remaining calculations to produce e are similar.
1. When free-spacing regex definition mode is used spaces must be enclosed in character classes (as I've done) or protected in some other way, as all unprotected spaces are removed. Note that a space--followed by a plus sign--is present in the conventionally-written regex. Free-spacing mode has the advantage that it is self-documenting.
2. See String#scan, particularly the treatment of regex groups, Array#unshift and Enumerable#each_cons.
3. See Enumerator#next.
Related
The question says:
Given an array of integers.
Return an array, where the first element is the count of positives numbers and the second element is sum of negative numbers. 0 is neither positive nor negative.
If the input is an empty array or is null, return an empty array.
def count_positives_sum_negatives(last)
pos = []
neg = []
x = lst.each
if x % 2 == 0
pos.push x
else neg.push x
end
y = pos.count
z = neg.sum
puts "[#{y},#{z}]"
end
I tested with
count_positives_sum_negatives([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -11, -12, -13, -14, -15])
#should return [10, -65]
I am unsure why my one only gives an error message:
An error occurred while loading spec_helper.
Failure/Error: if (x % 2) == 0
NoMethodError:
undefined method `%' for #<Enumerator: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, -11, -12, -13, -14, -15]:each>
# ./lib/solution.rb:7:in `count_positives_sum_negatives'
# ./lib/solution.rb:18:in `<top (required)>'
# ./spec/spec_helper.rb:1:in `require'
# ./spec/spec_helper.rb:1:in `<top (required)>'
No examples found.
No examples found.
You also certainly meant lst.each do |x| rather than x = lst.each. You get the error you do because lst.each without a block returns an Enumerator object, and that does not respond to %.
def count_positives_sum_negatives(lst)
pos = []
neg = []
lst.each do |x|
if x % 2 == 0
pos.push x
else
neg.push x
end
end
y = pos.count
z = neg.sum
puts "[#{y},#{z}]"
end
Note: x % 2 == 0 can be written as x.even?
But you're looking at evens and odds, and your description says count positives and sum negatives. You can use #positive? and #negative? for this, along with #partition.
def count_positives_sum_negatives(lst)
pos, neg = lst.partition(&:positive?)
y = pos.count
z = neg.sum
puts "[#{y},#{z}]"
end
You could also use #each_with_object to give you the info you're looking for.
def count_positives_sum_negatives(lst)
lst.each_with_object([0, 0]) do |x, arr|
if x.positive?
arr[0] += 1
else
arr[1] += x
end
end
end
#Chris has given the reason for your problem.
I think I would be inclined to write it as follows.
def count_positives_sum_negatives(lst)
non_pos = lst.reject(&:positive?)
[lst.size - non_pos.size, non_pos.sum]
end
count_positives_sum_negatives [-1, 2, 0, 0, 1, -4, -7, 0, 6]
#=> [3, -12]
This has the weakness that a temporary array is created (non_pos), but lst.size - non_pos.size and non_pos.sum are both relatively fast calculations, the latter because it is implemented in C. One would have to benchmark, however.
lst.reject(&:positive?) is a short form of lst.reject { |n| n.positive? ).
See Array#reject and Array#sum.
Okay, this final one I've done outputs the correct answer, but I think the portal itself is broken as it cannot tell that [10, -65] is the same as [10, -65]
enter image description here
def count_positives_sum_negatives(lst)
pos = []
neg = []
lst.each do |x|
if x.positive?
pos.push x
else
neg.push x
end
end
y = pos.count
z = neg.sum
final = []
final << y
final << z
print final
end
I'm an absolute coding beginner and have just started to learn ruby. I had a challenge where I was supposed to check if a number was "colorful".
When in a given number, product of every digit of a sub-sequence are different. That number is called Colorful Number.
For example, "263 is a colorful number because [2, 6, 3, 2*6, 6*3, 2*6*3] are all different; whereas 236 is not colorful, because [2, 3, 6, 2*3, 3*6, 2*3*6] have 6 twice.
So take all consecutive subsets of digits, take their product and ensure all the products are different."
In the challenge given we were to only accept numbers up to three digits.
So since I am a beginner I tried to write every product seperately.
I know this is not "good" code, but still I want to understand why it is not working. I think it should... but it doesn't :)
I would be so glad if someone could tell my why this is not working.
I am not looking for a nicer solution, I really just want to know why mine doesn't work.
THANK YOU SO MUCH!
def colorful?(number)
if number.class != Integer
return false
elsif number.to_s.length > 3
return false
elsif number == 0 || 1
return true
end
digits_arr = number.digits.reverse
product_1 = digits_arr[0]*digits_arr[1]
product_2 = digits_arr[1]*digits_arr[2]
product_3 = digits_arr[0]*digits_arr[1]*digits_arr[2]
final_array = digits_arr + product_1 + product_2 + product_3
if final_array.uniq.length == final_array.length
return true
else
return false
end
end
So, your biggest issue is here:
elsif number == 0 || 1
This means "when number is equal to 0 OR when 1 is truthy value". In Ruby, 1 is always a truthy value, so this condition is always satisfied, leading to the execution of return true, terminating your method and returning true for any value.
Once you replace it with either
elsif number == 0 || number == 1
or
elsif [0,1].include? number
you will still have some more issues to fix - next one will be No implicit conversion of Integer into an Array when you try to add number to an Array. I hope you'll figure this one out by yourself. :)
While we're here, few minor notes.
In Ruby, we prefer duck-typing over strong types (mostly becuase we don't have strong typing) so any form of argument type checks like if number.class != Integer is thrown upon (not to mention it won't work very well as you will reject a large number of other Numeric classes - if you have to chack for classes use is_a? or kind_of?). Note that if your method is ever executed with something that is not a number, you do want to throw an exception rather than let the calculation continue - it is much easier to prevent your system from getting into a bad state than debugging the bad state and guessing how you got there.
Might be personal opinion here, but elsif sucks - almost always there is a better way. In this scenario, since you're returning in each case, you could just do:
return false unless number.is_a? Integer # really unnecessary
return false if number.to_s.length > 3
return true if number == 0 || number == 1
require 'set'
def colorful?(n)
digits = n.digits.reverse
products = Set.new
(1..digits.size-1).each do |n|
digits.each_cons(n) { |a| return false unless products.add?(a.reduce(:*)) }
end
true
end
colorful? 263
#=> true
colorful? 236
#=> false
colorful? 3245
#=> true
See Set#add?
I can best show how the method works by adding some puts statements and executing the method for two of the example integers above.
def colorful?(n)
digits = n.digits.reverse
puts "digits = #{digits}"
products = Set.new
(1..digits.size-1).each do |n|
puts "n = #{n}"
puts "digits.each_cons(#{n}).to_a #=> #{digits.each_cons(n).to_a}"
digits.each_cons(n) do |a|
m = a.reduce(:*)
puts " a = #{a}, m = #{m}"
puts " products = #{products}"
puts " products.include?(#{m}) = #{products.include?(m)}"
return false unless products.add?(m)
end
end
puts "true is returned"
true
end
colorful? 263
#=> true
The following is displayed.
digits = [2, 6, 3]
n = 1
digits.each_cons(1).to_a #=> [[2], [6], [3]]
a = [2], m = 2
products = #<Set: {}>
products.include?(2) = false
a = [6], m = 6
products = #<Set: {2}>
products.include?(6) = false
a = [3], m = 3
products = #<Set: {2, 6}>
products.include?(3) = false
n = 2
digits.each_cons(2).to_a #=> [[2, 6], [6, 3]]
a = [2, 6], m = 12
products = #<Set: {2, 6, 3}>
products.include?(12) = false
a = [6, 3], m = 18
products = #<Set: {2, 6, 3, 12}>
products.include?(18) = false
true is returned
colorful? 236
#=> false
The following is displayed.
digits = [2, 3, 6]
n = 1
digits.each_cons(1).to_a #=> [[2], [3], [6]]
a = [2], m = 2
products = #<Set: {}>
products.include?(2) = false
a = [3], m = 3
products = #<Set: {2}>
products.include?(3) = false
a = [6], m = 6
products = #<Set: {2, 3}>
products.include?(6) = false
n = 2
digits.each_cons(2).to_a #=> [[2, 3], [3, 6]]
a = [2, 3], m = 6
products = #<Set: {2, 3, 6}>
products.include?(6) = true
Because products was found to contain m (3), false was returned.
So in the end I've solved it like this (I know there are nicer ways, but still, it now works (as in can tell if a number is colorful or not, if it has no more than 3 digits):
def colorful?(number)
return false unless number.is_a? Integer
digits_arr = number.digits.reverse
return false if digits_arr.length > 3
return true if digits_arr.length == 1
final_array = []
final_array << digits_arr
case digits_arr.length
when 3
product_one = digits_arr[0] * digits_arr[1]
final_array << product_one
product_two = digits_arr[1] * digits_arr[2]
final_array << product_two
product_three = digits_arr[0] * digits_arr[1] * digits_arr[2]
final_array << product_three
when 2
product_one = digits_arr[0] * digits_arr[1]
final_array << product_one
end
if final_array.flatten.uniq == final_array.flatten
return true
else
return false
end
end
I'm iterating over permutations of a list (18 items) like this:
List = [item0..item18] # (unpredictable)
Permutation_size = 7
Start_at = 200_000_000
for item, i in List.repeated_permutation(Permutation_size).each_with_index
next if i < Start_at
# do stuff
end
Start_at is used to resume from a previously saved state so it's always different but it takes almost 200s to reach 200 million so I'm wondering if there is a faster way to skip multiple iterations or start at iteration n (converting the enumerator to an array takes even longer). If not, a way to create a custom repeated_permutation(n).each_with_index (that yields results in the same order) would also be appreciated.
Feel free to redirect me to an existing answer (I haven't found any)
PS. (what I had come up with)
class Array
def rep_per_with_index len, start_at = 0
b = size
raise 'btl' if b > 36
counter = [0]*len
# counter = (start_at.to_s b).split('').map {|i| '0123456789'.include?(i) ? i.to_i : (i.ord - 87)} #this is weird, your way is way faster
start_at.to_s(b).chars.map {|i| i.to_i b}
counter.unshift *[0]*(len - counter.length)
counter.reverse!
i = start_at
Enumerator.new do |y|
loop do
y << [counter.reverse.map {|i| self[i]}, i]
i += 1
counter[0] += 1
counter.each_with_index do |v, i|
if v >= b
if i == len - 1
raise StopIteration
else
counter[i] = 0
counter[i + 1] += 1
end
else
break
end
end
end
end
end
end
I first construct a helper method, change_base, with three arguments:
off, the base-10 offset into the sequence of repeated permutations of the given array arr,
m, a number system base; and
p, the permutation size.
The method performs three steps to construct an array off_m:
converts off to base m (radix m);
separates the digits of the base m value into an array; and
if necessary, pads the array with leading 0s to make it of size p.
By setting m = arr.size, each digit of off_m is an offset into arr, so off_m maps the base-10 offset to a unique permutation of size p.
def change_base(m, p, off)
arr = off.to_s(m).chars.map { |c| c.to_i(m) }
arr.unshift(*[0]*(p-arr.size))
end
Some examples:
change_base(16, 2, 32)
#=> [2, 0]
change_base(16, 3, 255)
#=> [0, 15, 15]
change_base(36, 4, 859243)
#=> [18, 14, 35, 31]
18*36**3 + 14*36**2 + 35*36**1 + 31
#=> 859243
This implementation of change_base requires that m <= 36. I assume that will be sufficient, but algorithms are available to convert base-10 numbers to numbers with arbitrarily-large bases.
We now construct a method which accepts the given array, arr, the size of each permutation, p and a given base-10 offset into the sequence of permutations. The method returns a permutation, namely, an array of size p whose elements are elements of arr.
def offset_to_perm(arr, p, off)
arr.values_at(*change_base(arr.size, p, off))
end
We can now try this with an example.
arr = (0..3).to_a
p = 2
(arr.size**p).times do |off|
print "perm for off = "
print " " if off < 10
print "#{off}: "
p offset_to_perm(arr, p, off)
end
perm for off = 0: [0, 0]
perm for off = 1: [0, 1]
perm for off = 2: [0, 2]
perm for off = 3: [0, 3]
perm for off = 4: [0, 1]
perm for off = 5: [1, 1]
perm for off = 6: [2, 1]
perm for off = 7: [3, 1]
perm for off = 8: [0, 2]
perm for off = 9: [1, 2]
perm for off = 10: [2, 2]
perm for off = 11: [3, 2]
perm for off = 12: [0, 3]
perm for off = 13: [1, 3]
perm for off = 14: [2, 3]
perm for off = 15: [3, 3]
If we wish to begin at, say, offset 5, we can write:
i = 5
p offset_to_perm(arr, p, i)
[1, 1]
i = i.next #=> 6
p offset_to_perm(arr, p, i)
[2, 1]
...
In the following code:
def solve(a0, a1, a2, b0, b1, b2)
#score index: 0 = james, 1 = sam
score = Array.new(2, 0)
calcScore = lambda do |x,y|
if ( x > y )
score[0] += 1
end
if ( x < y )
score[1] += 1
end
end
0.upto 2 do |index|
calcScore.call(eval("a#{index}"),eval("b#{index}"))
end
score
end
Is there a more eloquent DRY way to achieve the dynamic variable reference without using:
eval("a#{index}")
While local_variable_get and eval could seem to do their job here, the right approach would be:
def solve(a0, a1, a2, b0, b1, b2)
a, b = [a0, a1, a2], [b0, b1, b2]
# deal with score
0.upto 2 do |index|
calcScore.call(a[index], b[index])
end
score
end
Or, better and more DRY:
def solve(*as_and_bs)
raise unless as_and_bs.size == 6
a, b = as_and_bs(0..2), as_and_bs(3..5)
# deal with score
0.upto 2 do |index|
calcScore.call(a[index], b[index])
end
score
end
Use binding.local_variable_get:
0.upto 2 do |index|
calcScore.call(binding.local_variable_get("a#{index}"),
binding.local_variable_get("b#{index}"))
end
eval is evil. Don't use it. Here's an equivalent code, which should work for any number of scores. It uses the fact that a <=> b returns -1, 0 or 1.
Your input format isn't very convenient. This code uses each_slice and transpose to transform [1,2,3,4,5,6] into [[1, 4], [2, 5], [3, 6]]. You can then iterate over the games to calculate the total score:
def calc_score(a, b)
[[0, 0], [1, 0], [0, 1]][a <=> b]
end
def solve(*scores)
size = scores.size
raise 'Need an even number of scores' unless size.even?
raise 'Need at least two scores' unless size > 0
scores.each_slice(size / 2).to_a.transpose.inject([0, 0]) do |(a_total, b_total), (a, b)|
a_score, b_score = calc_score(a, b)
[a_total + a_score, b_total + b_score]
end
end
or even shorter :
def solve(*scores)
size = scores.size
raise 'Need an even number of scores' unless size.even?
raise 'Need at least two scores' unless size > 0
scores.each_slice(size / 2).to_a.transpose.map do |a, b|
calc_score(a, b)
end.transpose.map{ |s| s.inject(:+) } # .map(&:sum) in Ruby 2.4
end
As an example:
solve(1, 2, 3, 4, 5, 6)
# [0, 3]
solve(2, 0, 0, 3)
# [1, 1]
If you combine a1, a2, and a3 into an array and do the same thing with b, then you can use regular [] indexing:
def solve(a, b)
#score index: 0 = james, 1 = sam
score = Array.new(2, 0)
calcScore = lambda do |x,y|
if ( x > y )
score[0] += 1
end
if ( x < y )
score[1] += 1
end
end
0.upto 2 do |index|
calsScore.call(a[index], b[index])
end
score
end
You could also add a custom error checking for the array lengths:
raise(ArgumentError) unless [a,b].all? { |arr| arr.length == 3 }
I'm running through TestFirstRuby and I've been stuck on problem 12, building a Reverse Polish Notation calculator. I've gotten through all of the tests except for the last one, asking me to take a string ("1 2 3 * +" and then "1 2 3 * + 4 5 / -"), and then evaluate the expression.
What I'm trying to do is convert the string to an array, changing the numbers into integers and the operators into symbols, then go through the array and evaluate the expression anytime it comes to an operator.
Here's the relevant part of the code:
def initialize
#expression = ''
end
def tokens(string)
tokens = string.split(' ')
tokens.map! { |digit| /[0-9]/.match(digit) ? digit.to_i : digit.to_sym }
end
def evaluate(string)
#1 2 3 * +
#1 2 3 * + 4 5 - /
total = 0
#expression = tokens(string)
#expression.map!{|item|
index = #expression.index(item)
if item == :+
total = total + (#expression[index-1] + #expression[index-2])
2.times {#expression.delete_at(index-1)}
elsif item == :-
total = total + (#expression[index-1] - #expression[index-2])
2.times {#expression.delete_at(index-1)}
elsif item == :x
total = total + (#expression[index-1] * #expression[index-2])
2.times {#expression.delete_at(index-1)}
elsif item == :/
total = total + (#expression[index-1].to_f / #expression[index-2])
2.times {#expression.delete_at(index-1)}
end
}
total
end
What I WANT to happen is this: for each item in the array, it checks if it matches any of the symbols. If there's a match, it changes the element two spaces back from the symbol into the value of whatever the expression is (so 2 3 * becomes 5 3 *). Then, I"m trying to delete the operator and the integer immediately before it, leaving only the evaluated value. I'm doing this by running delete_at twice on the index just before the operator (ideally, 5 3 * goes to 5 * and then just 5). Then it'll move on to the next element in the array.
What I THINK is going wrong, and am having trouble fixing: I think something's going on with the variable scope here. I'm trying to get the expression to be permanently changed every time it runs the code on whatever element it's currently on in the each loop. For each element, a variable 'index' is set using #expression.index(item). This should reset for each element in the each loop. I THINK what's going on is that the original #expression array is being called on for each iteration of the each loop, unchanged from each iteration of the each loop.
The error I'm getting is saying that when it gets to the '+' at the end of the first test string ('1 2 3 * +'), it's trying to add using :x, meaning that when it's calling for the two variables to add together (#expression[index-1] + #expression[index-2]), it's pulling the symbol, which I thought should have been deleted from #expression already. So what I'm hoping will evaluate as 6 + 1 is being evaluated as 3 + :x, which wouldn't work. It's pulling elements from the original array, instead of pulling from the array as it's changed.
Hopefully I'm explaining this clearly enough. Any advice would be great. I'm thinking there's something with scope going on, but I can't find anything specific to this kind of problem to help me out. I've tried different ways of coding this (.map, .each_with_index, .map.with_index, and others), and I'm getting the same problem each time.
You have a tremendous amount of redundant code. In particular, you replicate the operations for each of the four operators. Here is a more Ruby-like way of implementing your calculator.
Code
def evaluate(string)
arr = create_arr(string)
until arr.size == 1
i = arr.index(arr.find { |e| e.is_a? Symbol })
arr[i-2] = arr[i-2].send(arr[i], arr[i-1])
arr.delete_at(i)
arr.delete_at(i-1)
end
arr.first
end
def create_arr(string)
string.split(/\s+/).map { |e| e =~ /-?[0-9]+/ ? e.to_i : e.to_sym }
end
The line in create_arr could alternatively end : e } (sent accepts a string or symbol for the method), in which case e.is_a? Symbol would be changed to e.is_a? String.
Examples
evaluate("3 4 * 2 / 3 - 2 *") #=> 6
evaluate("10 2 / 3 + 2 / 2 -") #=> 2
evaluate("8 -2 / 1 +") #=> -3
evaluate("5 1 2 + 4 * + 3 -") #=> 14
Explanation
Suppose
string = "2 3 4 * 2 / +"
Step 1
arr = create_arr(string) #=> [2, 3, 4, :*, 2, :/, :+]
arr.size == 1 #=> false
v = arr.find { |e| e.is_a? Symbol } #=> :*
i = arr.index(v) #=> 3
arr[i-2] = arr[i-2].send(arr[i], arr[i-1])
# arr[1] = arr[1].send(arr[3], arr[2])
# arr[1] = 3.send(:*, 4) #=> 12
arr #=> [2, 12, 4, :*, 2, :/, :+]
arr.delete_at(i) #=> :*
arr #=> [2, 12, 4, 2, :/, :+]
arr.delete_at(i-1) #=> 4
arr #=> [2, 12, 2, :/, :+]
Step 2
arr.size == 1 #=> false
v = arr.find { |e| e.is_a? Symbol } #=> :/
i = arr.index(v) #=> 3
arr[i-2] = arr[i-2].send(arr[i], arr[i-1])
# arr[1] = arr[1].send(arr[3], arr[2])
# arr[1] = 12.send(:/, 2) #=> 6
arr #=> [2, 6, 2, :/, :+]
arr.delete_at(i) #=> :/
arr #=> [2, 6, 2, :+]
arr.delete_at(i-1) #=> 2
arr #=> [2, 6, :+]
Step 3
arr.size == 1 #=> false
v = arr.find { |e| e.is_a? Symbol } #=> :+
i = arr.index(v) #=> 2
arr[i-2] = arr[i-2].send(arr[i], arr[i-1])
# arr[0] = arr[0].send(arr[2], arr[1])
# arr[0] = 2.send(:+, 6) #=> 8
arr #=> [8, 6, :+]
arr.delete_at(i) #=> :+
arr #=> [8, 6]
arr.delete_at(i-1) #=> 6
arr #=> [8]
Step 4
arr.size == 1 #=> true
arr.first #=> 8