Here's a basic structure you could use.
Menu
First let's create a helper method to display the menu. Doing so speeds testing and makes it easier to change the menu in future.
MENU = [{ label: "Add Item Charge", text: 'item', type: :item },
{ label: "Add Labor Charge", text: 'labor', type: :labor}]
def display_menu
puts "Menu ID \tMenu Item"
puts "-------\t\t----------"
MENU.each.with_index(1) { |h,i| puts "#{i}\t\t#{h[:label]}" }
puts "0\t\tExit application"
end
Let's try it.
display_menu
Menu ID Menu Item
------- ----------
1 Add Item Charge
2 Add Labor Charge
0 Exit application
Note that to make changes to the menu items only MENU
needs to be altered.
For now, disregard the keys :text
and :type
in MENU
.
Adding charges
If the user wishes to add an item charge the following method will be called with an argument that is a label for the charge type and will return a valid dollar amount:
def item_charge(charge_type)
loop do
print "Enter #{charge_type} amount: $"
case (amt = Float(gets, exception: false))
when nil
puts " Entry must be in dollars or dollars and cents"
when -Float::INFINITY...0
puts " Entry cannot be negative"
else
break amt
end
end
end
See Kernel::Float. This method was given the :exception
option in Ruby v2.7. Earlier versions raised an exception if the string could not be converted to a float. That exception would have to be rescued and nil
returned.
Here's a possible dialogue:
item_charge('labor')
Enter labor amount: $213.46x
Entry must be in dollars or dollars and cents
Enter labor amount: $-76
Entry cannot be negative
Enter labor amount: $154.73
#=> 154.73
Save charges
Now let's create a hash to hold the total cost of items and the cost of labor, with values initialized to nil
.
costs = MENU.each_with_object({}) { |g,h| h[g[:type]] = nil }
#=> {:item=>nil, :labor=>nil}
This will be the first line of the method enter_costs
below.
OK to exit?
Let's assume we don't want to allow the user to exit the application before they have entered a labor charge and at least one item charge. We might write a separate method to see if they meet that requirement:
def ok_to_exit?(costs)
h = MENU.find { |h| costs[h[:type]].nil? }
puts " You have not yet entered a #{h[:text]} cost" unless h.nil?
h.nil?
end
See Enumerable#find.
Is menu selection valid?
We need to see if numerical value entered for the menu id selection (other than zero, which we have already checked for) is valid:
def menu_selection_valid?(menu_id)
valid = (1..MENU.size).cover?(menu_id)
puts " That is not a valid menu id" unless valid
valid
end
menu_selection_valid?(4)
That is not a valid menu id
#=> false
menu_selection_valid?(-3)
That is not a valid menu id
#=> false
menu_selection_valid?(2)
#=> true
Body
We can now put everything together.
def enter_costs
costs = MENU.each_with_object({}) { |g,h| h[g[:type]] = nil }
loop do
display_menu
print ("Enter Menu ID: ")
menu_id = gets().to_i
if menu_id.zero?
ok_to_exit?(costs) ? break : next
end
next unless menu_selection_valid?(menu_id)
h = MENU[menu_id-1]
case h[:type]
when :labor
if costs[h[:type]].nil?
costs[h[:type]] = item_charge(h[:text])
else
puts " You have already entered the labor charge"
next
end
when :item
costs[h[:type]] = (costs[h[:type]] ||= 0) + item_charge(h[:text])
end
end
costs
end
Here is a possible dialog.
enter_costs
Menu ID Menu Item
------- ----------
1 Add Item Charge
2 Add Labor Charge
0 Exit application
Enter Menu ID: 0
You have not yet entered a item cost
< menu redisplayed >
Enter Menu ID: 1
Enter item amount: $2.50
< menu redisplayed >
Enter Menu ID: 0
You have not yet entered a labor cost
< menu redisplayed >
Enter Menu ID: 9
That is not a valid menu id
< menu redisplayed >
Enter Menu ID: 2
Enter labor amount: $1345.61
< menu redisplayed >
Enter Menu ID: 1
Enter item amount: $3.12
< menu redisplayed >
Enter Menu ID: 2
You have already entered the labour charge
< menu redisplayed >
Enter Menu ID: 0
#=> {:item=>5.62, :labor=>1345.61}
Notice that the numbering of items in the menu is done in code, so that changes to MENU
will not necessitate changes to those menu item numbers elsewhere in the code.
I suggest using Kernel#loop, together with keyword break, for all loops, in preference to while
, until
and (especially) for
loops.