def route_action(action)
case action
when 1 then #meals_controller.add
when 2 then #meals_controller.list
when 3 then #meals_controller.edit
when 4 then #meals_controller.delete
when 5 then #customers_controller.add
when 6 then #customers_controller.list
when 7 then #customers_controller.edit
when 8 then #customers_controller.delete
when 0 then stop
else
puts "Please press 1, 2, 3, 4, 5, 6, 7, 8 or 0"
end
end
So I want to reduce this case when is there another way of implementing this?
Heads up, this is just a general suggestion on how to use a bit of Ruby meta-programming to get out of the tangle of having to declare a long method full of case business logic. It won't fit perfectly and will need to extra work to do, for instance, the "quit" logic.
Also. I'll reiterate what's been said in one of the direct answers to the post. Your case solution is VERY clear. It's good code and we shouldn't jump into messier thing just to comply to the all-mighty gods of the ruby stylesheet guidelines. Just because a method is under 10 lines, it doesn't make it automatically better than an 11 (or... hell 40) lines one.
Now...
Here's a meta-programming suggestion...
You can define a hash on a constant to hold the variables needed for the business logic:
ROUTES = [
{ action: :add, controller: :meals, description: "Add a meal" },
{ action: :list, controller: :meals, description: "List all meals" },
{ action: :add, controller: :customers, description: "Add a customers" },
{ action: :list, controller: :customers, description: "List all customers" },
]
You can then create a method that dispatches the user to the correct controller action using the hash info:
def dispatch(action_index)
route_action = ROUTES[action_index][:action]
route_controller = ROUTES[action_index][:controller]
instance_variable_get("##{route_controller}_controller").send(route_action)
end
It's very easy with the hash to iterate over it to display the route descriptions:
def display_options
ROUTES.each_with_index do |route, index|
puts "#{index + 1}. #{route[:description]}"
end
end
You can at some point in your code dispatch the user to the relevant controller action with dispatch(gets.chomp.to_i - 1).
Cool thing about the hash is that you can always add more routes to it by just adding a single line.
One possibility would be to have an array of message names to send based on the action input which is used as an index, and by determining which controller to send to by dividing by 5. Inputs from 1 to 4 will yield 0, while 5 to 8 will yield 1.
def route_action
actions = [:add, :list, :edit, :delete]
case action
when 1..8
controller = action / 5 < 1 ? #meals_controller : #customers_controller
msg = actions[(action - 1) % 4]
controller.send(msg)
when 0
stop
else
puts "Please press 1, 2, 3, 4, 5, 6, 7, 8 or 0"
end
end
How about:
CASES = [#meals_controller, #customers_controller].product([:add, :list, :edit, :delete])
def route_action(action)
if action > 0
selected = CASES[action-1] # something like [:#meals_controller, :add]
if selected
selected.first.send(*selected.last)
else
puts "action number too large"
end
elsif action == 0
stop
else # Do we have to catch this?
puts "action number negative" # programming error?
end
end
Related
Currently, I'm having print like this
print ((stamp_amount[0], 'first mark') unless stamp_amount[0].zero?), (', ' if !stamp_amount[0].zero? && !stamp_amount[1].zero?),
((stamp_amount[1], 'second mark') unless stamp_amount[1].zero?)
stamp_amount is an array with 2 integer values
Let's say in the current situation stamp_amount[0] = 10 and stamp_amount[1] = 3
Output preview:
10 first mark, 3 second mark
So if stamp_amount[0] = 0 the 10 first mark, part won't be show. Same if stamp_amount[1] = 0 the , 3 second mark part won't be shown
For me, it seems a little bit incorrect in terms of theory. Could you please suggest me the more correct or less painful print of this? :)
Cheers!
Your code is trying to join a sequence of up to two elements with a separator. The joining is a solved problem, see Array#join.
The problem can be then reduced to "how can I produce the correct sequence, given my stamp_amount input". Now this can be done in a thousand ways. Here's one:
def my_print(stamp_amount)
ary = [
!stamp_amount[0].zero? && stamp_amount[0],
!stamp_amount[1].zero? && stamp_amount[1],
].select{|elem| elem }
ary.join(', ')
end
my_print([10, 3]) # => "10, 3"
my_print([0, 3]) # => "3"
my_print([10, 0]) # => "10"
my_print([0, 0]) # => ""
Here's another
ary = []
ary << stamp_amount[0] unless stamp_amount[0].zero?
ary << stamp_amount[1] unless stamp_amount[1].zero?
ary.join(', ')
Here's yet another. This version can handle stamp_amount of any length.
ary = stamp_amount.reject(&:zero?)
ary.join(', ')
I'd go with the third, but the second one may be the easiest to understand for a beginner.
Use the select, as an alternative to reject (shown in part 3 of the answer by Sergio Tulentsev). It is just asa readable, and depending on the context and on the future changes to the code, you may prefer one versus the other.
puts stamp_amount.select{ |a| !a.zero? }.join(", ")
A few examples of inputs and outputs are:
stamp_amount output
--------------------------------------------------------------------------
10, 3 10, 3
10, 0 10
0, 3 3
0, 0 (prints an empty line, because the selected array is empty)
You're calculating zero? on index points more often than is needed, but the first thing I would look at refactoring here is the readability of the code. It might be nicer to calculate the message to print outside of the print method and explain what is happening with variable names.
# rubocop is going to complain about variable assignment like this
first_amount, second_amount = *stamp_amount
We can actually use the reason rubocop prefers the .zero? over == 0 or .empty? method to guide our development. zero? is in essence just empty? but it communicates the meaning of what you are attempting to do in a better manner. I would use this reasoning when assigning strings to variables that explain what they are doing.
some_name_that_explains_what_this_is_0 = "#{first_amount} piecu centu marka"
some_name_that_explains_what_this_is_1 = "#{second_amount} tris centu marka"
Your current code is confusing as you have the possibility of printing a string like "10 tris centu marka" which does not make lexical sense and probably not what you are after considering tis evaluates to 'second mark', which would pose an issue if the first value is zero. We also could reject zero integers before we start converting them to strings.
array = [1, 0].reject(&:zero?)
Now we can take the array and do something like:
string = []
array.each_with_index { |e, i| string << "#{e} #{Ordinalize.new(i).ordinalize} mark" }
message = string.join(', ')
print(message)
# ord class
class Ordinalize
def initialize(value)
#value = value
end
def ordinalize
mapping[#value]
end
def mapping
# acounting for zero index
['first', 'second']
end
end
where we are calculating the ordinalization and letting our new class handle the sentence structure for us.
Outputs:
[1, 0] => "1 first mark"
[0, 1] => "1 first mark"
[1, 2] => "1 first mark, 2 second mark"
I'm writing a method which will assess statistical information relative to distributional parameters:
def munch(data:, mean:, standard_deviation:)
# munch the data
end
However, standard_deviation is quite a mouthful, and is known by other names (e.g., sigma, sd) which may be more memorable or convenient for end-users. I could add more parameters to the method and then manually check that one and only one of the alternative names is used, but that's tedious and ugly. Is there some way to say that sd and sigma are aliases for standard_deviation so that the following would work?
result1 = munch(data: my_data, mean: 20.0, standard_deviation: 3.0)
result2 = munch(data: my_data, mean: 20.0, sd: 3.0)
p result1 == result2 # => true
If you don't want to type too much, and really want to sanity-check, something like this might work?
def alias_param(opts, *keys, **args)
supplied = opts.select { |k, _| keys.include?(k) }
if supplied.size > 1
raise ArgumentError, "Multiple aliases supplied: #{supplied.keys.inspect}"
elsif supplied.size < 1
raise ArgumentError, "Missing argument: one of #{keys.inspect}"
end
opts.values.first
end
def fn(data:, mean:, **opts)
sd = alias_param(opts, :sd, :sigma, :standard_deviation)
end
fn(data: 1, mean:3)
To directly answer your question, no, at least not in the automatic way I believe you mean by your comments.
There is no built-in mechanism to define multiple names for one option, and have Ruby automatically return the same value no matter which of those options you reference. By your comment, I assume this is the intended behavior, something akin to an alias keyword, and then having the ability to call either by old or new name.
Your only real option is to parse the options manually, which typically shouldn't add too much boring boiler-plate code:
def blending(**options)
#mode = options[:mode] || options[:blend_mode]
#function = options[:function] || options[:blend_function]
end
There is nothing similar to alias_option :mode, :blend_mode that will allow you to simply use one of those names and get the value no matter which the method was invoked with. No matter which way you do this, there will be some manual checking to be done.
Based on Amadan's answer, here's what I ended up going with.
def check4aliases(opts, *keys)
supplied = opts.select { |k, _| keys.include?(k) }
return supplied.values.first if supplied.size == 1
msg = supplied.empty? ?
"Missing parameter: #{keys.first}" : "Use only one of #{keys.join(', ')}"
raise ArgumentError, msg
end
def munch(data:, **opts)
parameters = {
mu: check4aliases(opts, :mean, :mu),
sd: check4aliases(opts, :standard_deviation, :sd, :sigma)
}
# do actual work with data, mu, and sd
end
test_cases = [
{mean: 3, sd: 7},
{mu: 5, sigma: 4},
{mean: 3, mu: 3, sd: 7, sigma: 7, standard_deviation: 7},
{mean: 3, sd: 7, sigma: 8},
{mean: 3},
]
test_cases.each do |parms|
begin
p munch(data: 1, **parms)
rescue ArgumentError => msg
puts msg
end
end
which produces:
{:mu=>3, :sd=>7}
{:mu=>5, :sd=>4}
Use only one of mean, mu
Use only one of standard_deviation, sd, sigma
Missing parameter: standard_deviation
I still wish aliasing was an option, but this is usable.
I have a very simple Ruby implementation of a game called "FizzBuzz" (i.e. given an input number, it returns "Fizz" if the number is multiple of 3, "Buzz" if multiple of 5, "FizzBuzz" if multiple of both and the original number if it doesn't fit any of the previous condition):
class FizzBuzz
def answer(number)
multiple3 = number%3 == 0
multiple5 = number%5 == 0
return case
when (multiple3 and multiple5) then "FizzBuzz"
when multiple3 then "Fizz"
when multiple5 then "Buzz"
else number
end
end
end
I wrote a test using RSpec to validate each one of the conditions:
require "rspec"
require "./fizzBuzz"
RSpec.describe "#answer" do
it "returns Buzz when number is multiple of 3" do
result = FizzBuzz.new.answer(3)
expect(result).to eq("Fizz")
end
it "returns Buzz when number is multiple of 5" do
result = FizzBuzz.new.answer(5)
expect(result).to eq("Buzz")
end
it "returns a number when the input number is neither multiple of 3 nor 5" do
result = FizzBuzz.new.answer(11)
expect(result).to eq(11)
end
end
The test works perfectly, however, I'm using concrete values in it (i.e. 3, 5 and 11).
My question is: what if I wanted to test my FizzBuzz Ruby script using a wide range of values (e.g. from 1 to 10000)?
I know I can solve this by using each loops and cases directly in RSpec, however, my concern is that if in my test I adopt the same conditional statements as in the Ruby script to be tested (i.e. when number%3 == 0 then "Fizz", etc.) I will end up testing my code using an RSpec script that follows exactly the same logic as the script to be tested, hence the test will probably pass successfully.
What would be the alternative? Are there best practices to write tests using a wide pool of values (e.g. using a loop) rather than hard-coded or specific values?
A possible half-way point here is to loop through possible answers in your RSpec tests. Keeping your code DRY is important but so is keeping your tests DRY and this is sometimes under-estimated.
How about something like this:
RSpec.describe "#answer" do
expected_values = {'3': 'Fizz', '5': 'Buzz', '6': 'Fizz', '11': '11', '15': 'FizzBuzz'}
expected_values.each do |val, expected|
it "returns #{expected} when number is #{val}" do
result = FizzBuzz.new.answer(val.to_i)
expect(result).to eq(expected)
end
end
end
That way, you can add tests easily by adding them to the expected_values hash, but if the method name changed or something similar you would only have to change it in one place
In support of the accepted answer I would suggest wrapping the wider test in a context block, so that the test remains isolated from other code:
RSpec.describe "#answer" do
context 'when testing inputs and answers' do
RESULTS = { 3 => "Fizz",
5 => "Buzz",
11 => 11 }
RESULTS.each do |value, answer|
it "returns #{answer} when the input is #{value}" do
result = FizzBuzz.new.answer(value)
expect(result).to eq(answer)
end
end
end
end
RSpec now has built-in cool features like shared examples. More info here.
As a result we'll get shared group like this (note, that this group can be used in other tests):
shared_examples "shared example" do |number, result|
it "returns #{result} when accepts #{number}" do
# implicit `subject` here is equal to FizzBuzz.new
expect(subject.answer(number)).to eq(result)
end
end
And tests will be more readable and written in more "rspec-like" way:
DATA_SET = {15 => 'FizzBuzz', 3 => 'Fizz', 5 => 'Buzz', 11 => 11}
RSpec.describe FizzBuzz do
context "#answer" do
DATA_SET.each do |number, result|
# pseudo-randomizing test data.
number = [1,2,4,7,8,11,13,14,16].sample*number
result = number if result.is_a?(Integer)
include_examples "shared example", number, result
end
end
end
I will pretend that you are asking in case you will really need it and this simple case is just for illustration.
There is Property Based Testing who can resolve this kind of issue in an elegant manner, with this approach you have to find some property (for example in the addition of the two number the result is superior to the two number and a + b = b + a...). And you will use a framework who will generate randomly the entries in order to cover a larger spectra than if it was a specific cases.
There is framework who can helps in Ruby as well.
An excellent explanation of Property Based Testing http://fsharpforfunandprofit.com/posts/property-based-testing/ (disclaimer the code is in Fsharp)
You can use cycles inside your specs:
RSpec.describe "#answer" do
RESULTS = { 3 => "Fizz",
5 => "Buzz",
11 => 11 }
RESULTS.each do |value, answer|
it "returns #{answer} when the input is #{value}" do
result = FizzBuzz.new.answer(value)
expect(result).to eq(answer)
end
end
end
I got the task to create a lottery program which outputs 6 random numbers from 1 to 49 without duplicates. I'm not allowed to use the shuffle-Method of Arrays and as a tip I recieved that I should create an array with 49 entries and "shuffle" the numbers. I thought about this tip but it's not really helping.
This is what I got till now, I hope someone understands my code. I'm still stuck in a more Java way of writing than in ruby.
# Generating the Array
l_num = Array.new(6)
i = 0
while (i == 0)
l_num << [rand(1...49), rand(1...49), rand(1...49), rand(1...49), rand(1...49), rand(1...49)]
if (l_num.uniq.length == l_num.length)
i += 1
end
end
#Output
puts 'Todays lottery numbers are'
l_num.each { |a| print a, " " }
I picked up the uniq-Methode, because I read that it could elimante doubles that way, but I don't think it works in this case. In previous versions of my code I got a Error because I was trying to override the allready created array, I understand why Ruby is giving me an error but I have no clue how to do it in another way.
I hope someone is able to provide me some code-pieces, methods, solutions or tips for this task, thanks in advance.
This is the strategy I would take:
lottery_numbers = []
begin
# add 1 because otherwise it gives you numbers from 0-48
number = rand(49)+1
lottery_numbers.push(number) unless lottery_numbers.include?(number)
end while lottery_numbers.size < 6
puts "lottery numbers:"
puts lottery_numbers.join(" ")
Rubyists tend to initialize arrays with [] as opposed to the verbose Array.new
Charles, I think the easiest would be the following:
Array.new(49) { |x| x + 1 }.sample(6)
However this is what I believe you was prohibited to do? If yes, try a more "manual" solution:
Array.new(49) { |x| x + 1 }.sort { rand() - 0.5 }.take(6)
Otherwise, try implementing one completely "manual" solution. For example:
require 'set'
result = Set.new
loop do
result << 1 + rand(49)
break if result.size == 6
end
puts result.to_a.inspect
Here's one way to do it you can't use sample:
a = *1..49
#=> [1, 2, 3,..., 49]
6.times.map { a.delete_at(rand(a.size))+1 }
# => [44, 41, 15, 19, 46, 17]
min_lottery_number = 1
max_lottery_number = 49
total_size = 6
(min_lottery_number..max_lottery_number).to_a.sample(total_size)
I am trying to loop through multiple statements, but want to go through each one once, example:
while count < 5 do
count+= (not sure if this how ruby increments counts)
puts "In condition one"
next if count > 1
puts "In condition two"
next if count > 1
#..
end
Update 1:
Thanks for the reply, what I'm trying to do is loop through an array and have each element of the array be applied to 10 different conditions. For example: array[has 100 elements] element 1 gets condition 1, element 2 goes on to condition 2, and so on. Since there are 10 conditions, the 11th element in the array would get condition 1 again, and so on (condition 1 condition 2 condition 3 ...)
Update 2:
Thanks again for taking the time to reply. I apologize that I'm not being very clear. The array contains emails. I have 10 email servers and want to send the 200 emails I have in my array through each server (only 1 email per server). I hope that makes sense
If I'm reading you correctly, you want to send a large number of emails through a small number of servers while balancing the load. Try creating a class to manage the servers (here's the basic idea)
class ServerFarm
def initialize
#servers = []
end
attr_accessor :servers
def add_server(server)
#servers << server
end
def remove_server(x)
if x.is_a?(Numeric) then
#servers.delete_at(x)
elsif x.is_a?(Server)
#servers.delete(x)
end
end
def server_available?
#servers.each {|s| return true if s.available? }
false
end
def dispatch_message(email)
#servers.each_with_index {|s, i|
next unless s.available?
s.dispatch(email)
return i
}
nil
end
end
Now, all you will have to do is call ServerFarm.dispatch_message for an email and it will be sent using one of the available servers. This class assumes that you have a class named Server that holds the info for your individual servers, etc etc.
array = (1..100).to_a
conditions = (1..10).to_a
array.each_with_index do |elem, i|
puts "element %d, using condition %d" % [elem, conditions[i % conditions.length]]
end
produces
element 1, using condition 1
element 2, using condition 2
element 3, using condition 3
element 4, using condition 4
element 5, using condition 5
element 6, using condition 6
element 7, using condition 7
element 8, using condition 8
element 9, using condition 9
element 10, using condition 10
element 11, using condition 1
element 12, using condition 2
etc.
Does this help? I can't tell what you are trying to do.
5.times do |count|
puts 'In condition ' + %w(one two three four five)[count]
end
The 5.times do |count| will excecute the block five times with count starting at zero and incrementing each time. %w(one two three four five) is the same as ["one", "two", "three", "four", "five"].
If you want to do five different things consecutively, you do not need a loop. Just put the statements in a row:
# do thing 1
# do thing 2
# do thing 3
# ...
Edit:
"I have an array that I want to loop through, but each element in the array needs to go through a different condition each time and then restart at the first condition."
To loop through an array endlessly, testing each element against conditions:
arr = ['sdfhaq', 'aieei', 'xzhzdwz']
loop do
arr.each do |x|
case x
when /..h/
puts 'There was a \'h\' at the third character.'
when /.{6}/
puts 'There were at least six characters.'
else
puts 'None of the above.'
end
end
end
Edit 2:
"Thanks for the reply, what I'm trying to do is loop through an array and have each element of the array be applied to 10 different conditions, example: array[has 100 elements] element 1 gets condition 1 element 2 goes on to condition 2 and so on, since there are 10 conditions the 11th element in the array would get condition 1 again and so on. condition 1 condition 2 condition"
You will need to use the % method on numbers.
arr = Array.new(130) # an array of 130 nil elements.
num_conditions = 10
arr.each_with_index do |x, i|
condition = (i + 1) % num_conditions
puts "Condition number = #{condition}"
end
More information: http://ruby-doc.org/core/classes/Fixnum.html#M001059
Edit three:
def send_an_email(email, server)
puts "Sending an email with the text #{email.inspect} to #{server}."
end
email_servers = ['1.1.1.1', '2.2.2.2']
emails = ['How are you doing?', 'When are you coming over?', 'Check out this link!']
emails.each_with_index do |email, i|
send_an_email email, email_servers[i % email_servers.length]
end
You can modify email_servers and emails and have it still work, even if the lengths are changed.
array.each_slice(10) do |emails|
servers.zip(emails) { |server,email| server<<email }
end
(Ruby 1.9.2)