-
-
Save hchood/7633627 to your computer and use it in GitHub Desktop.
# Ruby Fundamentals 4: Challenge | |
require 'csv' | |
require 'date' | |
require 'table_print' | |
# DATA | |
products = [] | |
CSV.foreach('products.csv', headers: true) do |row| | |
products << { name: row[0], sku: row[1], price: row[2], wholesale_price: row[3] } | |
end | |
prices = [] | |
products.each do |product| | |
prices << [product[:name], product[:price], product[:wholesale_price]] | |
end | |
# prices = [["light", "5.0", "2.5"], ["medium", "7.5", "4"], ["bold", "9.75", "5"]] | |
# HELPER METHODS | |
def is_valid_action?(input) | |
/\A[12]\z/.match(input) | |
end | |
def is_valid_item?(input, products) | |
n = products.length | |
/\A[1-n]\z/.match(input) # how to deal with CSV of 10 or more products? | |
end | |
def is_valid_currency?(input) | |
/\A[0-9]+\.[0-9]{2}$/.match(input) | |
end | |
def is_valid_date?(input) | |
/\A[0-9]{2}\/[0-9]{2}\/[0-9]{4}$/.match(input) | |
end | |
# PROGRAM | |
puts "Welcome to James's coffee emporium!" | |
puts "1) Sell coffee to a customer" | |
puts "2) Get a report of past sales" | |
puts "What would you like to do today?" | |
action = gets.chomp | |
until is_valid_action?(action) | |
puts "What would you like to do today?" | |
action = gets.chomp | |
end | |
if action == "1" | |
puts "Now we are selling coffee!" | |
products.each_with_index do |product, index| | |
puts "#{index + 1}) Add item - $#{sprintf('%.2f' % product[:price])} - #{product[:name].capitalize} Bag" | |
end | |
puts "#{products.length + 1}) Complete Sale" | |
quantities = Array.new(products.length,0) | |
sale = [Time.new] | |
quantities.each do |quantity| | |
sale << quantity | |
end | |
def calc_subtotal(quantities, prices) | |
subtotal = 0 | |
quantities.each_with_index do |quantity, index| | |
subtotal += quantity * prices[index][1].to_f | |
end | |
subtotal | |
end | |
def ask_item(products) | |
puts "What item is being purchased?" | |
item = gets.chomp | |
until is_valid_item?(item, products) | |
puts "What item is being purchased?" | |
item = gets.chomp | |
end | |
item | |
end | |
item = ask_item(products) | |
num_items = products.length | |
until item == (num_items + 1).to_s | |
puts "How many bags?" | |
quantity = gets.chomp.to_i | |
sale[item.to_i] += quantity | |
quantities[item.to_i - 1] += quantity | |
current_subtotal = calc_subtotal(quantities, prices) | |
puts 'Subtotal: $' + sprintf('%.2f' % current_subtotal) | |
item = ask_item(products) | |
end | |
puts '===Sale Complete===' | |
def output_item_subtotals(quantities, prices) | |
quantities.each_with_index do |quantity, index| | |
item_subtotal = quantity * prices[index][1].to_f | |
puts "$#{sprintf('%.2f' % item_subtotal)} - #{quantity} #{prices[index][0].capitalize}" unless quantity == 0 | |
end | |
end | |
output_item_subtotals(quantities, prices) | |
puts "\nTotal: $" + sprintf('%.2f' % current_subtotal) | |
# calculate the change due, as before | |
puts 'What is the amount tendered?' | |
tendered = gets.chomp | |
unless is_valid_currency?(tendered) | |
puts 'WARNING: Invalid currency detected! Exiting...' | |
else | |
change = tendered.to_f - current_subtotal | |
if change < 0 | |
print 'WARNING: Customer still owes $' + sprintf('%.2f' % change.abs) | |
puts '! Exiting...' | |
else | |
CSV.open('sales.csv', 'a') do |csv| | |
csv << sale | |
end | |
puts "\n===Thank You!===" | |
puts 'The total change due is $' + sprintf('%.2f' % change) | |
puts Time.now.strftime('%D %r') | |
puts '================' | |
end | |
end | |
else | |
# REPORTING SECTION | |
sales = CSV.read('sales.csv') | |
puts "Enter date (in MM/DD/YYYY format):" | |
user_date = gets.chomp | |
until is_valid_date?(user_date) | |
puts "Enter date (in MM/DD/YYYY format):" | |
user_date = gets.chomp | |
end | |
date_array = user_date.split('/') | |
date_requested = Time.new(date_array[2],date_array[0],date_array[1]).inspect[0..9] | |
current_date = Time.new.inspect[0..9] | |
daily_sales = sales.select { |sale| sale[0][0..9] == date_requested } | |
if date_requested > current_date | |
puts "Sorry, you've requested a date in the future." | |
elsif daily_sales.length == 0 | |
puts "There were no sales on that date." | |
else | |
# Reporting method definitions | |
def items_in_sale(sale) | |
quantities = sale[1..-1] | |
num_items = quantities.map { |quant| quant.to_i }.reduce {|a,e| a + e} | |
end | |
def total_items(sales) | |
array_items = sales.map { |sale| items_in_sale(sale) } | |
array_items.reduce {|a,e| a + e } | |
end | |
def gross_per_sale(sale, prices) | |
quantities = sale[1..-1] | |
gross = 0 | |
quantities.each_with_index do |quant, index| | |
gross += quant.to_i * prices[index][1].to_f | |
end | |
gross | |
end | |
def total_sales(sales, prices) | |
array_gross = sales.map { |sale| gross_per_sale(sale, prices) } | |
array_gross.reduce {|a,e| a + e } | |
end | |
def cost_per_sale(sale, prices) | |
quantities = sale[1..-1] | |
cost = 0 | |
quantities.each_with_index do |quant, index| | |
cost += quant.to_i * prices[index][2].to_f | |
end | |
cost | |
end | |
def total_costs(sales, prices) | |
array_costs = sales.map { |sale| cost_per_sale(sale, prices) } | |
array_costs.reduce {|a,e| a + e } | |
end | |
# Creating and outputting report | |
items_summary = total_items(daily_sales) | |
sales_summary = total_sales(daily_sales, prices) | |
profit_summary = sales_summary - total_costs(daily_sales, prices) | |
puts '======================' | |
puts "Summary for #{ date_requested }" | |
puts '======================' | |
puts "Total items: #{ items_summary }" | |
puts 'Gross sales: $' + sprintf('%.2f' % sales_summary) | |
puts 'Net profit: $' + sprintf('%.2f' % profit_summary) | |
sales_table = [] | |
daily_sales.each_with_index do |sale, index| | |
row = {"Date" => sale[0][0..9], | |
"Time" => sale[0][11,5], | |
"\# items" => items_in_sale(sale), | |
"Gross sales" => sprintf('%.2f' % gross_per_sale(sale, prices)), | |
"Cost" => sprintf('%.2f' % cost_per_sale(sale,prices))} | |
sales_table << row | |
end | |
puts | |
tp sales_table | |
end | |
end |
When working with dates and parsing them from user input you can use the strptime
method on the Date
class:
require 'date'
today = Date.strptime('10/24/2013', '%m/%d/%Y')
# => #<Date: 2013-10-24 ((2456590j,0s,0n),+0s,2299161j)>
The first argument to strptime
is the string containing the date (e.g. from the user's input or the CSV file) and the second argument is the format that you're expecting the date to be in (i.e. "month/day/year"). Converting to a Date
object makes working with dates significantly easier rather than just checking if the strings are equal (e.g. you can compare whether another date comes before or after this one if you wanted to check for ranges).
Since we only care about the date portion of the timestamp, checking two date objects for equality will return true
if the days are the same (it doesn't check the times):
a = Date.parse('2013-10-24 13:10:04')
b = Date.parse('2013-10-24 06:45:00')
a == b
# => true
This can be used when filtering for sales on a given day:
date_requested = Date.strptime(user_input, '%m/%d/%Y')
daily_sales = sales.select { |sale| Date.parse(sale) == date_requested }
The Date.parse
is a simplified version of strptime
that parses strings in UTC date format (YYYY-MM-DD HH:MM:SS
) which is how they're saved in the CSV.
When deciding how to store your data it's important to think about what information you have now and how it might change over time. In this case the sales are stored in a CSV format where each column represents a quantity sold for a particular product:
2013-11-24 21:37:09 -0500,0,2,1
This represents a sale with 2 units of the second product and 1 unit of the third product. The problem is that the user can add more products by editing products.csv
which means you'll need additional columns in sales.csv
to represent those products, even if none are actually sold:
2013-11-24 21:37:09 -0500,0,2,1
2013-11-24 21:39:29 -0500,0,0,0,2
For a few products it is not a big deal but if you can imagine supporting thousands of products then each row in sales.csv
will have to store thousands of quantities even if a customer just purchased a single product. Also, since the quantities are based on position the user wouldn't be able to remove or rearrange entries in products.csv
without affecting how sales.csv
is interpreted.
A more flexible approach might be to store each item type purchased on its own row:
purchase_time,item_sku,quantity
2013-11-24 21:37:09 -0500,2,2
2013-11-24 21:37:09 -0500,3,1
2013-11-24 21:39:29 -0500,4,2
Here a single order can span multiple lines if a customer purchased multiple types of coffee but we won't need to add additional columns to support those multiple types. We can also tell which items were purchased in the same order since the purchase_time
should be the same for all of the items in a single order.
In addition, if a customer only purchases a single item then we can represent that by storing that item's SKU and quantity without having to record all of the other item quantities. This means that the amount of space required to record a sale does not grow as more products are added to products.csv
.
That's super helpful. Thanks Adam!
This looks great! Nice use of methods for validating input. They have intuitive names and make the program easier to follow. I have a few suggestions which I'll leave in additional comments on this gist.