0

I have written some code out of the Ruby Pickaxe book and I am trying to get it to work. (around page 62 of "Programming Ruby The Pragmatic Programmer's Guide") **Edit: More info on the book: (C) 2009, for Ruby 1.9

Given this error message, I am not quite sure how to identify what is going wrong. I appreciate any help in understanding what is going wrong here.

How does one know what to identify and solve? I am wondering if Ruby's CSV functionality is really just this easy-- no gem/bundle install to run?

I would really like to be able to run my test_code.rb file, but I am unable to figure out this error.

Thank you for your time, Patrick


Note: all of these files are in the same directory.

IRB command, followed by the error message it generates:

2.1.1 :005 > load "test_code.rb"
LoadError: cannot load such file -- csv-reader
    from /Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from /Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from test_code.rb:3:in `<top (required)>'
    from (irb):5:in `load'
    from (irb):5
    from /Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/bin/irb:11:in `<main>'

I don't know how relevant this is, based on the error message, but thought I'd include it. kernel_require.rb line 55:

if Gem::Specification.unresolved_deps.empty? then
  begin
    RUBYGEMS_ACTIVATION_MONITOR.exit
    return gem_original_require(path)
  ensure
    RUBYGEMS_ACTIVATION_MONITOR.enter
  end
end

line 9-11 of irb:

require "irb"

IRB.start(__FILE__)

First file of program: csv-reader.rb

require 'csv'
require 'book-in-stock'

class CsvReader

  def initialize
    @books_in_stock = []
  end

  def read_in_csv_data(csv_file_name)
    CSV.foreach(csv_file_name, headers: true) do |row|
      @books_in_stock << BookInStock.new(row["ISBN"], row["Amount"])
    end
  end

  def total_value_in_stock
    sum = 0.0
    @books_in_stock.each {|book| sum += book.price}
  end

  def number_of_each_isbn
  end

end

Second file: book-in-stock.rb

class BookInStock
  attr_reader :isbn
  attr_accessor :price

  def initialize(isbn, price)
    @isbn = isbn
    @price = Float(price)
  end

  def price_in_cents
    Integer(price*100 + 0.5)
  end

  def price_in_cents=(cents)
    @price = cents / 100.0
  end
end

Third file: stock-stats.rb

require 'csv-reader'
reader = CsvReader.new
ARGV.each do |csv_file_name|
  STDERR.puts "Processing #{csv_file_name}"
  reader.read_in_csv_data(csv_file_name)
end

puts "Total value = #{reader.total_value_in_stock}"

Fourth file: test_code.rb

# this is the test code file

require 'csv-reader'
require 'book-in-stock'
require 'stock-stats'

# code to call
reader = CsvReader.new
reader.read_in_csv_data("file1.csv")
reader.read_in_csv_data("file2.csv")

puts "Total value in stock = #{reader.total_value_in_stock}"


# code to call
book = BookInStock.new("isbn1", 33.80)
puts "Price = #{book.price}"
puts "Price in cents = #{book.price_in_cents}"
book.price_in_cents = 1234
puts "Price = #{book.price}"
puts "Price in cents = #{book.price_in_cents}"

CSV files: file1.csv

ISBN, Amount
isbn1, 49.00
isbn2, 24.54
isbn3, 33.23
isbn4, 15.55

file2.csv

ISBN, Amount
isbn5-file2, 39.98
isbn6-file2, 14.84
isbn7-file2, 43.63
isbn8-file2, 25.55


Edit

After Frederick Cheung's suggestion to change require to require_relative (for all but the 1st line of csv-reader.rb), the script is running, but a method is not working (see below)

(I did receive an error about this line: @price = Float(price) and changed it to @price = price.to_f and it runs just fine. )

3 Questions:

-> I changed the header of my csv files to "ISBN, Amount". Previously Amount was amount (not capitalized). Does this matter (i.e. the capitalizing of the header)?

-> While we're on the subject, what is the "row" keyword doing in the following #read_in_csv_data method?

-> Now that my code runs it appears that the output for "Total value in stock" is not summing up all of the prices in the csv file. Could a Rubyist please help me understand why this is happening?

The method

  def read_in_csv_data(csv_file_name)
    CSV.foreach(csv_file_name, headers: true) do |row|
      @books_in_stock << BookInStock.new(row["ISBN"], row["Amount"])
    end
  end

and call seem fine to me...

reader = CsvReader.new
reader.read_in_csv_data("file1.csv")
reader.read_in_csv_data("file2.csv")

Here is the current output from terminal:

Total value = []
Price = 33.8
Price in cents = 3380
Price = 12.34
Price in cents = 1234
Total value in stock = [#<BookInStock:0xb8168a60 @isbn="isbn1", @price=0.0>, #<BookInStock:0xb8168740 @isbn="isbn2", @price=0.0>, #<BookInStock:0xb8168358 @isbn="isbn3", @price=0.0>, #<BookInStock:0xb81546f0 @isbn="isbn4", @price=0.0>, #<BookInStock:0xb8156a18 @isbn="isbn5-file2", @price=0.0>, #<BookInStock:0xb8156784 @isbn="isbn6-file2", @price=0.0>, #<BookInStock:0xb81564a0 @isbn="isbn7-file2", @price=0.0>, #<BookInStock:0xb8156248 @isbn="isbn8-file2", @price=0.0>]

Thanks again.

Edit: Big thanks to 7Stud for a very thorough followup answer on every question I had. You have been exceptionally helpful. I have learned several important things thanks to your post.

Edit:

Still not able to get the code to run.

I am not sure how to add to / edit the $LOAD_PATH, so I tried putting all of the files into this directory: directory: ~MY_RUBY_HOME/lib/ruby/site_ruby/2.1.0/csv-reader (i.e. /Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0/csv-reader)

However, I still receive the same error message:

 ✘  ~MY_RUBY_HOME/lib/ruby/site_ruby/2.1.0/csv-reader  ruby test_code.rb file1.csv file2.csv
/Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require': cannot load such file -- ./csv_reader (LoadError)
    from /Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
    from test_code.rb:1:in `<main>'
Patrick Meaney
  • 129
  • 2
  • 12

2 Answers2

2

require searches for files in Ruby's load path (this is stored in the global variables $: or $LOAD_PATH)

The current directory is not in the load path by default (it used to be in ruby 1.8 and earlier) which is why ruby says that it can't find csv-reader

You can add to the load path either by manipulating the $: variable (it behaves just like an array) or with the the -I option.

For example if you launch irb by doing

irb -I.

Then your code should run without modification (assuming there are no other problems with it)

Lastly you could switch your require statements to use require_relative - this locates files relative to the current file

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • Frederick, I greatly appreciate your help, thank you. Could you break it down for a newb? For example, How would I manipulate the $: variable? e.g. I found this page: http://stackoverflow.com/questions/837123/adding-a-directory-to-load-path-ruby and I wonder if I could use one of those methods, and simply put it into my file named csv-reader.rb, and add it above the "require 'csv'" line. Also, I was unsure how to use the command "irb -I" in relation to the command "load "test_code.rb"". Thanks. – Patrick Meaney Jun 14 '14 at 21:56
  • I have made some edits. `irb -I` Is just how you would launch irb. Also added a note about require_relative - the question you link to is essentially reinventing require_relative which didn't exist at the time) – Frederick Cheung Jun 14 '14 at 22:21
  • If you go down the require relative route you you need to edit all the places you require your own files – Frederick Cheung Jun 14 '14 at 22:24
2

I have written some code out of the Ruby Pickaxe book

Yeah, but there are many Ruby Pickaxe books.

IRB command, followed by the error message it generates:

NEVER run anything in IRB. Never use IRB for ANYTHING. Instead put your code in a file, and then run the file, e.g:

$ ruby my_prog.rb

LoadError: cannot load such file -- csv-reader

If the files you want to require are not located in the directories ruby searches automatically(to see those directories execute the line `p $LOAD_PATH'), then you can specify the absolute or relative path to the file you want to require in the require statement:

require './book_in_stock'

I did receive an error about this line: @price = Float(price) and changed it to @price = price.to_f and it runs just fine.

x = 'hello'
p x.to_f
p Float(x)

--output:--
0.0
1.rb:3:in `Float': invalid value for Float(): "hello" (ArgumentError)
    from 1.rb:3:in `<main>

The difference between Float() and to_f() is that Float will raise an exception when it is unable to convert the String to a Float, while to_f() will return 0 when it cannot convert the String to a Float. Unless you know what you are doing, it's probably best to use Float(), so that you are alerted to the fact that your data has an error in it.

While we're on the subject, what is the "row" keyword doing in the following #read_in_csv_data method?

When you loop through the rows of your file(e.g. CSV.foreach), csv converts one row of your file into a thing called a "CSV::Row", and then assigns the "CSV::ROW" object to the loop variable, which you have named "row":

CSV.foreach(csv_file_name, headers: true) do |row|
                                               ^
                                               |

So "row" is a variable that refers to a "CSV::Row". A "CSV::Row" acts like a hash, enabling you to write things like row['ISBN'] to retrieve the value in that column.

Spaces are significant in csv files. If your header row is ISBN, Amount, then the column names are "ISBN" and " Amount" (see the leading space?). That means there is no value for

row['Amount']

i.e. it will return nil, but there is a value for

row[' Amount']
     ^
     |

Now that my code runs it appears that the output for "Total value in stock" is not summing up all of the prices in the csv file. Could a Rubyist please help me understand why this is happening?

1) A def returns the value of the last statement that was executed in the def.

2) Array#each() returns the array.

Here is your def:

  def total_value_in_stock
    sum = 0.0
    @books_in_stock.each {|book| sum += book.price}
  end

That def returns the @books_in_stock array. You need to return the sum:

  def total_value_in_stock
    sum = 0.0
    @books_in_stock.each {|book| sum += book.price}
    sum
  end

If you want to get tricky, you can have csv automatically convert any data in your file that looks like a number to a number:

CSV.foreach(
  csv_file_name, 
  headers: true,
  :converters => :numeric
) do |row| ...

...then your BookInStock class would look like this:

class BookInStock
  attr_reader :isbn
  attr_accessor :price

  def initialize(isbn, price)
    @isbn = isbn
    @price = price  #Float(price)
  end

Here are all your files amended so they will run correctly:

csv_reader.rb:

require 'csv'
require './book_in_stock'

class CsvReader

  def initialize
    @books_in_stock = []
  end

  def read_in_csv_data(csv_file_name)
    CSV.foreach(csv_file_name, headers: true) do |row|
      @books_in_stock << BookInStock.new(row["ISBN"], row["Amount"])
    end
  end

  def total_value_in_stock
    sum = 0.0
    @books_in_stock.each {|book| sum += book.price}
    sum
  end

  def number_of_each_isbn
  end

end

stock_stats.rb:

require './csv_reader'

reader = CsvReader.new

ARGV.each do |csv_file_name|
  STDERR.puts "Processing #{csv_file_name}"
  reader.read_in_csv_data(csv_file_name)
end

puts "Total value = #{reader.total_value_in_stock}"

test_code.rb:

require './csv_reader'
require './book_in_stock'
require './stock_stats'

reader = CsvReader.new
reader.read_in_csv_data("file1.csv")
reader.read_in_csv_data("file2.csv")

puts "Total value in stock = #{reader.total_value_in_stock}"


# code to call
book = BookInStock.new("isbn1", 33.80)
puts "Price = #{book.price}"
puts "Price in cents = #{book.price_in_cents}"
book.price_in_cents = 1234
puts "Price = #{book.price}"
puts "Price in cents = #{book.price_in_cents}"

book_in_stock.rb:

class BookInStock
  attr_reader :isbn
  attr_accessor :price

  def initialize(isbn, price)
    @isbn = isbn
    @price = Float(price)
  end

  def price_in_cents
    Integer(price*100 + 0.5)
  end

  def price_in_cents=(cents)
    @price = cents / 100.0
  end
end

file1.csv:

ISBN,Amount
isbn1,49.00
isbn2,24.54
isbn3,33.23
isbn4,15.55

file2.csv:

ISBN,Amount
isbn5-file2,39.98
isbn6-file2,14.84
isbn7-file2,43.63
isbn8-file2,25.55

Now run the program:

~/ruby_programs$ ruby test_code.rb file1.csv file2.csv
Processing file1.csv
Processing file2.csv
Total value = 246.32
Total value in stock = 246.32
Price = 33.8
Price in cents = 3380
Price = 12.34
Price in cents = 1234
7stud
  • 46,922
  • 14
  • 101
  • 127
  • This is awesome help. I hate to be a total newb, but: I'm not familiar with Ruby's file structure or where the CSV library is. 1. How would I correctly load the path in this particualar script (or where can I find the CSV lib?)? 2. if I keep all my Ruby code in e.g. ~/code/ruby how can I get this into the file path by default? --Truncated p $LOAD_PATH for reference: ["/Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0", "/Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby/2.1.0/x86_64-darwin12.0", "/Users/patrickmeaney/.rvm/rubies/ruby-2.1.1/lib/ruby/site_ruby"] – Patrick Meaney Jun 15 '14 at 23:45
  • Is there some sort of $: << File.expand_path(File.dirname(__FILE__) + “/../somedirectory”) code I need to place at the top of csv-reader.rb? Or... Ultimately, I want to solve this issue so that it doesn't happen again. Would it be most easily solved if I place my ruby files into a certain directory within the load path? Do you suggest one? Thanks – Patrick Meaney Jun 16 '14 at 00:07
  • @Patrick Meaney, 1. Why do you care where ruby stores its Standard Library files(e.g. csv)? 2. http://stackoverflow.com/questions/2900370/why-does-ruby-1-9-2-remove-from-load-path-and-whats-the-alternative – 7stud Jun 16 '14 at 21:38
  • Re: #1. I have no idea what the solution is, so I was just trying to guess about what could potentially be involved. Again, I'm a newb :D Thx for your help. – Patrick Meaney Jun 17 '14 at 16:49
  • 1
    @PatrickMeaney, Okay. For ruby's standard library modules, you don't need to do anything other than `require 'stl_mod_name'`. Here is a list of all ruby's Standard Library modules: http://www.ruby-doc.org/stdlib-2.1.2/. The `'Pickaxe'` also has all the STL modules in the back with some examples for each one. – 7stud Jun 17 '14 at 22:29
  • Thx man. Then, do you think the solution is to have this: "require File.expand_path(File.join(File.dirname(__FILE__), 'csv'))" as the first line of my csv-reader.rb file? Basically, I am wondering if the problem is that the file is unable to find the CSV library, or if the CSV library is unable to find my file. – Patrick Meaney Jun 17 '14 at 22:38
  • @PatrickMeaney, 1) You need to specify who you are directing your comments to, e.g. `@7stud`, otherwise the person won't see your comment unless they happen to come back to the thread. When you write `@7stud`, stackoverflow will send me a notification. – 7stud Jun 20 '14 at 01:01
  • @Patrick Meaney, 2)Your error has nothing to do with ruby's csv module: `LoadError: cannot load such file -- csv-reader`. The csv-reader file is your file. The csv module isn't looking for your file. The `ruby` command is looking for the csv-reader file you told it to require--and ruby can't find it. 3) I posted all your files with corrections (put them all in the same directory), then follow the instructions I posted for running test_code.rb. – 7stud Jun 20 '14 at 01:02
  • @PatrickMeaney, 4) `I tried putting all of the files into this directory: directory: ~MY_RUBY_HOME/lib/ruby/site_ruby/2.1.0` Don't ever do that. That dir is for ruby's internal use--not yours. – 7stud Jun 20 '14 at 01:05
  • @PatrickMeaney, `I am not sure how to add to / edit the $LOAD_PATH`. First, have you checked your spelling? Is your csv_reader file actually named something different than the spelling you are using in the require statement? $LOAD_PATH is a variable that refers to an Array whose elements are Strings. The Strings are directory names that the ruby command will search for files you require. You can add elements to an Array using the push() method (or alternatively you can use the synonym '<<', which is more common), so `$LOAD_PATH << '.'` – 7stud Jun 20 '14 at 01:11
  • @PatrickMeany, `I tried putting all of the files into this directory...` Just do this: `$ cd ~`, then do `$ mkdir ruby_programs`, then do `$ cd ruby_programs`, then put all your files in that directory. – 7stud Jun 20 '14 at 01:15