<file_sep>require_relative 'find_by'
require_relative 'errors'
require 'csv'

PATH_TO_DATA = File.dirname(__FILE__) + "/../data/data.csv"
METHODS_TO_METAPROGRAM = ["find_by_brand", "find_by_name"]

class Udacidata
	
	# Create a new object, update the CSV accordingly, and return the new object
	def self.create(attributes = nil)
		csv_data_ids = self.all.map{|p| p.id}
  	unless csv_data_ids.include?(attributes[:id])
  		last_id_in_db = csv_data_ids.last ? csv_data_ids.last : -1
  		attributes[:id] = last_id_in_db + 1
			CSV.open(PATH_TO_DATA, "ab") {|csv| csv << [attributes[:id].to_s, attributes[:brand].to_s, attributes[:name].to_s, attributes[:price].to_s]} 
  	end
  	return_object = self.new(attributes)
	end

	# Return an array of all of the objects in the CSV
  def self.all
  	data = CSV.read(PATH_TO_DATA, 'rb').drop(1)
  	data.map{|arr| self.new(id: arr[0].to_i, brand: arr[1], name: arr[2], price: arr[3].to_f)}
  end

  # Return an array of the first n objects in the CSV
  def self.first(n=1)
  	objects = self.all
		n = limit_to_number_of_objects(n, objects)
  	objects = objects[0..n-1]
	  objects.length == 1 ? objects.first : objects
  end

	# Return an array of the last n objects in the CSV
  def self.last(n=1)
  	objects = self.all[(-1*n)..-1]
  	n = limit_to_number_of_objects(n, objects)
  	objects.length == 1 ? objects.first : objects
  end

  # Limit the number n to be a maximum of the length of the objects array
  def self.limit_to_number_of_objects(n, objects)
  	n > objects.length ? objects.length : n
  end

  # Find and return an object from the CSV by id
  def self.find(target_id, objects=nil)
  	objects ||= self.all
    if objects.map{|o| o.id}.include?(target_id)
  		objects.each {|obj| return obj if obj.id == target_id}
  	else
  		raise(ProductNotFoundError, "Find failed, there is no product with id: #{target_id}.")
  	end
	end

	# Remove an object from the CSV by id, and return the removed object
	def self.destroy(target_id)
    object_to_delete = self.find(target_id)
    objects = self.all.delete_if{|o| o.id == target_id}
    self.write_objects_to_csv(objects)
    object_to_delete
	end

	# Return the array of objects that match the attributes specified by the target hash
	def self.where(target)
		self.all.select do |o|
			is_match = true
			target.each do |k, v|
				is_match = false if o.send(k) != v
			end
			is_match
		end
	end

	# Update an object's attribute(s), both in memory and in the CSV
	def update(hash_of_updates)
		hash_of_updates.each do |k, v|
			self.instance_eval("@#{k.to_s}='#{v}'")
		end
		self.class.update_object_in_csv(self)
		self
	end

	# Update the passed in object in the CSV
	def self.update_object_in_csv(updated_object)
		objects = self.all
		objects = objects.map{|o| o.id == updated_object.id ? updated_object : o}
		self.write_objects_to_csv(objects)
	end

	# Overwrite the CSV with a new CSV containing all of the objects in "objects"
	def self.write_objects_to_csv(objects)
    CSV.open(PATH_TO_DATA, "wb") do |csv|
    	csv << ["id", "brand", "product", "price"]
    	objects.each do |o|
    		csv << [o.id.to_s, o.brand, o.name, o.price.to_s]	
    	end
    end
	end

	# Improve the readability of objects' string representation
	def to_s
		"<id: #{self.id}, brand: #{self.brand}, name: #{self.name}, price: #{self.price}"
	end

end
<file_sep>class Module

	# Create the find_by methods using meta-programming
  def create_finder_methods(*attributes)
    attributes.each do |attribute_name|
    	self.class_eval("def find_by_#{attribute_name}(target_string); self.all.find{|o| o.#{attribute_name} == target_string}; end")
  	end
  end

	# Override method missing to catch the find_by_* methods and define them using metaprogramming
	def method_missing(method_sym, *arguments, &block)
		if self.respond_to?(method_sym)
			Module.create_finder_methods(method_sym.to_s.split("_")[2])
			send(method_sym, arguments.first)
		else
			super
		end
	end

	# Override responds_to? to accurately reprsent that the class responds to the find_by_* methods defined at runtime
	def respond_to?(method_sym, include_private = false)
	  if METHODS_TO_METAPROGRAM.include?(method_sym.to_s)
	    true
	  else
	    super
	  end
	end

end
<file_sep>require 'faker'
require 'csv'
require_relative '../lib/product'

# This file contains code that populates the database with
# fake data for testing purposes

def db_seed

	# Seed the CSV "database"
	# Column order: id, brand, product, price
	10.times do |i|
		Product.create(brand: Faker::Commerce.department, name: Faker::Commerce.product_name, price: Faker::Number.decimal(2))
	end

end<file_sep># Define a ProductNotFoundError for use in the find(n) and destroy(n) methods
class ProductNotFoundError < StandardError
end<file_sep>module Analyzable

	# Return the average price of the objects in the array passed in
  def average_price(objects_array)
  	(objects_array.map{|o| o.price}.reduce(:+) / (objects_array.length * 1.0)).round(2)
  end

  # Return the sales report as a string
  def print_report(objects_array)
  	report = "\n"
	  report += "  /$$$$$$            /$$                           /$$$$$$$                                            /$$    \n"
	  report += " /$$__  $$          | $$                          | $$__  $$                                          | $$    \n"
	  report += "| $$  \\__/  /$$$$$$ | $$  /$$$$$$   /$$$$$$$      | $$  \\ $$  /$$$$$$   /$$$$$$   /$$$$$$   /$$$$$$  /$$$$$$  \n"
	  report += "|  $$$$$$  |____  $$| $$ /$$__  $$ /$$_____/      | $$$$$$$/ /$$__  $$ /$$__  $$ /$$__  $$ /$$__  $$|_  $$_/  \n"
	  report += " \\____  $$  /$$$$$$$| $$| $$$$$$$$|  $$$$$$       | $$__  $$| $$$$$$$$| $$  \\ $$| $$  \\ $$| $$  \\__/  | $$    \n"
	  report += " /$$  \\ $$ /$$__  $$| $$| $$_____/ \\____  $$      | $$  \\ $$| $$_____/| $$  | $$| $$  | $$| $$        | $$ /$$\n"
	  report += "|  $$$$$$/|  $$$$$$$| $$|  $$$$$$$ /$$$$$$$/      | $$  | $$|  $$$$$$$| $$$$$$$/|  $$$$$$/| $$        |  $$$$/\n"
	  report += " \\______/  \\_______/|__/ \\_______/|_______/       |__/  |__/ \\_______/| $$____/  \\______/ |__/         \\___/  \n"
	  report += "                                                                      | $$                                    \n"
	  report += "                                                                      | $$                                    \n"
	  report += "                                                                      |__/                                    \n"
	  report += "\n"
	  report += ".oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo..oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.oOo.\n" 
	  report += "\n"
	  report += "Total number of products: #{objects_array.length}\n"
	  report += "\n"
	  report += "Average product price: $#{average_price(objects_array)}\n"
	  report += "\n"
	  report += "Product counts by brand:\n"
	  count_by_brand(objects_array).each do |k, v|
		  report += "  #{k}: #{v}\n"
	  end
	  report += "\n"
	  report += "Product counts by name:\n"
	  count_by_name(objects_array).each do |k, v|
		  report += "  #{k}: #{v}\n"
	  end
	  report += "\n"
	  report
  end

  # Return a hash with a count of products by brand
	def count_by_brand(objects_array)
		count_by_attribute(objects_array, "brand")
	end

	# Return a hash with a count of products by name
	def count_by_name(objects_array)
		count_by_attribute(objects_array, "name")
	end	

	# Return a hash with a count of products by target_attribute
	def count_by_attribute(objects_array, target_attribute)
		counts_hash = {}
		objects_array.each do |o|
			if counts_hash.key?(o.send(target_attribute))
				counts_hash[o.send(target_attribute)] += 1
			else
				counts_hash[o.send(target_attribute)] = 1
			end
		end
		counts_hash
	end

end
