Ruby reads .50 as .5 - ruby

I have an Item class, and I initialized five variables. I am trying to match the output in my terminal to the value of the expected_summary. I am calling Item.summary in the following code:
class Item
attr_reader :name, :description, :manufacturer, :price, :summary
def initialize (name, manufacturer, price, description=nil, summary=nil)
#name = name
#manufacturer = manufacturer
#price = price
#description = description
if description
#summary = "Name: #{name}
Description: #{description}
Manufacturer: #{manufacturer}
Price: $#{price}"
else
#summary = "Name: #{name}
Manufacturer: #{manufacturer}
Price: $#{price}"
end
end
end
#expected_summary = %q(Name: Spy Notebook
#Manufacturer: Spys-R-Us
#Price: $10.50)
item = Item.new("Spy Notebook", "Spys-R-Us", 10.50)
puts item.summary
When I pass a number 10.50 as the price argument, it returns as 10.5. I cannot figure out why. Why does Ruby read 10.50 as 10.5? Is there a way to correct this?

The answer is in the string format operator which allows you to coerce the float into a string with 2 decimal places. This method will also work to do the rounding of 3 digit numbers, I didn't try it any further, but I'm fairly certain it will work. Here is your original code modified to showcase exactly how it would work.
class Item
attr_reader :name, :description, :manufacturer, :price, :summary
def initialize (name, manufacturer, price, description=nil, summary=nil)
#name = name
#manufacturer = manufacturer
#price = "%.2f" % price
#description = description
if description
#summary = "Name: #{name}
Description: #{description}
Manufacturer: #{manufacturer}
Price: $#{#price}"
else
#summary = "Name: #{name}
Manufacturer: #{manufacturer}
Price: $#{#price}"
end
end
def price
#price
end
end
EDIT: I didn't see #tadman's comment until after posting this, he beat me to the answer.

Maybe you can define your custom method to format the number as string with two decimals:
def two_decimals(number)
number = Float(number)
price_split = number.to_s.split('.')
price_split[1] = price_split.last + '0'*(2-price_split.last.size) if price_split.last.size < 2
price_split.join('.')
end
two_decimals(10.25) #=> "10.25"
two_decimals(10.2) #=> "10.20"
two_decimals(10) #=> "10.00"
Or something better...

The usual way of formatting numbers is:
'%.02f' % number
Where that is the printf-style notation for describing how you want something formatted. This is inherited from C and shows up in a lot of other languages:
'%f' % number # A floating-point number with default precision
'%.02f' % number # A floating-point number rounded to 2 places
Within Rails you also have helper methods like number_with_precision which can handle localization cases where the decimal separator is not a dot:
number_with_precision(number, precision: 2, locale: :fr)
# => 1,50

Related

How to stub/mock multiple options depending on user input with Rspec 3.4

I am completely new to Rspec, and it's my first time trying to test outside of the rails framework. I am simply trying to understand how I can possibly mock behavior of my app when the implementation is pretty complex.
I want to be able to mimic the behavior of calling customize_gender inputting a choice and checking that when 1 is entered the result is 'Male', when 2 is entered the result is 'Female', etc.
I also want to be able to check if the instance variable of #gender was correctly set, which is why I added the attr_reader :gender in the first place. I have been trying a few things, but I guess I do not understand how mocks in general work to be able to find a solution. I have looked at similar questions but they do not seem to work for my scenario. Any insight is greatly appreciated!
Main file (person.rb)
class Person
attr_reader :gender
GENDER = { male: 'Male', female: 'Female', other: 'Other'}
def initialize
puts customize_gender
end
def customize_gender
display_hash_option GENDER, 'What is your gender? '
choice = gets.chomp.to_i
#gender =
case choice
when 1
GENDER[:male]
when 2
GENDER[:female]
when 3
print 'Enter your preferred gender: '
gets.chomp.downcase
else
puts 'Error: Person -> customize_gender()'
end
end
private
def display_hash_option(hash, saying = '')
print saying
hash.each_with_index { |(key, _value), index| print "#{index.next}) #{key} " }
end
end
Rspec File (spec/person_spec.rb)
require_relative "../person"
describe Person do
let(:person) { Person.new }
allow(Person).to receive(:gets).and_return(1,2,3)
person.customize_gender
expect(person.gender).to eq 'Male'
# allow(person).to receive(:customize_gender).and_return('Male')
# expect(Person).to receive(:puts).with('What is your gender?')
# allow(Person).to receive(:gets) { 1 }
# expect(person.gender).to eq 'Male'
end
Here's how you could do it, the only thing mocked here is that gets is set to '1' (remember it's a string in this case as gets input is always a string)
RSpec.describe Person do
subject { Person.new }
it 'returns male as gender when male is chosen' do
allow(subject).to receive(:gets).and_return('1')
subject.customize_gender
expect(subject.gender).to eq('Male')
end
end
For when 3 you could use the following.
RSpec.describe Person do
subject { Person.new }
it 'returns other as gender when other has been entered' do
allow(subject).to receive(:gets).and_return('3', 'other')
subject.customize_gender
expect(subject.gender).to eq('other')
end
end

How to dump a class into another class to create a save state

I have a small class which is for a character and we can assign to it from outside the class.
I need to know how I can dump all the information in that class into another that can be used to create a YAML file.
require "yaml"
module Save
filename = "data.yaml"
character = []
sex = []
race = []
stats = [Str=[], Dex=[], Con=[], Int=[], Wis=[], Cha=[]]
inventory = []
saving_throws = [fortitude=[], reflex=[], will=[]]
#Armor Class, Flat footed Armor Class, and Touch armor Class
armor_class = [ac=[], fac=[], tac=[]]
armor_worn = [head=[], eyes=[], neck=[], shoulders=[], body=[], torso=[], arms_wrists=[], hands=[], ring1=[], ring2=[], waist=[], feet=[]]
money = []
god = []
speciality_school = [] #wizard
companion = [] #also used for familirs and psicrystals
skills = []
class_race_traits = []
feats = []
languages = []
program_data = {
character: character,
sex: sex,
race: race,
stats: stats,
inventory: inventory,
saving_throws: saving_throws,
armor_class: armor_class,
armor_worn: armor_worn,
mony: money,
god: god,
speciality_school: speciality_school,
companion: companion,
skills: skills,
class_race_traits: class_race_traits,
feats: feats,
languages: languages
}
File.write filename, YAML.dump(program_data)
end
This is the code I want to use to obtain the user content from the player:
class Character
attr_reader :name, :race, :description
def initialize (name, race, description)
#name = name
#race = race
#description = description
end
end
def prompt
print "Enter Command >"
end
puts "What is your name?"
prompt; name = gets.chomp.downcase
puts "What is your race?"
prompt; race = gets.chomp.downcase
puts "What do you look like?"
prompt; desc = gets.chomp.downcase
player_one = Character.new(name, race, desc)
puts player_one
I'm stuck on how to load it back and refill the character content to make it continue where the player left off.
Meditate on this bit of fictional code:
require 'yaml'
SAVED_STATE_FILE = 'saved_state.yaml'
class User
def initialize(name=nil, address=nil)
#name = name
#address = address
end
def to_h
{
'name' => #name,
'address' => #address
}
end
def save
File.write(SAVED_STATE_FILE, self.to_h.to_yaml)
end
def reload
state = YAML.load_file(SAVED_STATE_FILE)
#name, #address = state.values
end
end
We can create a new user with some properties:
user = User.new('Popeye', '123 E. Main St.')
# => #<User:0x007fe361097058 #name="Popeye", #address="123 E. Main St.">
To write that information to a file you should probably start by using YAML, which results in a very readable output and is readable by many different languages, making the data file reusable. A hash results in a very readable output:
user.to_h
# => {"name"=>"Popeye", "address"=>"123 E. Main St."}
user.to_h.to_yaml
# => "---\nname: Popeye\naddress: 123 E. Main St.\n"
Save the YAML serialized hash:
user.save
Create a new version of the user without any state:
user = User.new
# => #<User:0x007fe361094a88 #name=nil, #address=nil>
Load the saved information from the file back into the blank object:
user.reload
Which results in:
user
# => #<User:0x007fe361094a88 #name="Popeye", #address="123 E. Main St.">
That will give you enough to work from.
Your current code isn't going to work well though; I'd recommend reading some tutorials about Ruby classes and modules, as a Module isn't what you want, at least for your initial code.

How to create list of information and later search and sort that information?

I need to create simple book information list (database within code) which contain author, genre, name, release date and page number. And later search by author(sort by date, search by genre(sort by author). What is simplest way to do this ?
You can create a Book class and then store those objects in an Array. It's pretty straight forward but you may want to read a little about Ruby and Object Oriented Programming.
class Book
attr_accessor :name
attr_accessor :author
attr_accessor :genre
def initialize name, author, genre
#name = name
#author = author
#genre = genre
end
def to_s
"book name: " + #name + "\nauthor: " + #author + "\ngenre: " + #genre
end
end
# create a few books
book_one = Book.new("Scary Book","Michael", "horror")
book_two = Book.new("Long Book", "Jim", "comedy")
book_three = Book.new("Short Book", "Pam", "romance")
# store them in an Array for sorting and searching
book_array = [book_one, book_two, book_three]
# sort the array by author
sorted_array = book_array.sort_by {|book| book.author}
# find the book(s) in the "romance" genre
romance_books = book_array.select {|book| book.genre == "romance"}

Refactor the Ruby CLI program

I'm new to programming in Ruby.
How do I make the output show Revenue and Profit or Loss?
How can I refactor the following code to look neater? I know it's wrong but I have no idea how to take my if profit out of the initialize method.
class Theater
attr_accessor :ticket_price, :number_of_attendees, :revenue, :cost
def initialize
puts "What is your selling price of the ticket?"
#ticket_price = gets.chomp.to_i
puts "How many audience are there?"
#number_of_attendees = gets.chomp.to_i
#revenue = (#number_of_attendees * #ticket_price)
#cost = (#number_of_attendees * 3) + 180
#profit = (#revenue - #cost)
if #profit > 0
puts "Profit made: $#{#profit}"
else
puts "Loss incurred: $#{#profit.abs}"
end
end
end
theater = Theater.new
# theater.profit
# puts "Revenue for the theater is RM#{theater.revenue}."
# I hope to put my Profit/Loss here
#
# puts theater.revenue
Thanks guys.
Do not initialize the object with input from the user, make your object accept the needed values. Make a method to read the needed input and return you new Theater. Last of all put the if in separate method like #report_profit.
Remember constructors are for setting up the initial state of the object, making sure it is in a valid state. The constructor should not have side effects(in your case system input/output). This is something to be aware for all programming languages, not just ruby.
Try this:
class Theatre
COST = { running: 3, fixed: 180 }
attr_accessor :number_of_audience, :ticket_price
def revenue
#number_of_audience * #ticket_price
end
def total_cost
COST[:fixed] + (#number_of_audience * COST[:running])
end
def net
revenue - total_cost
end
def profit?
net > 0
end
end
class TheatreCLI
def initialize
#theatre = Theatre.new
end
def seek_number_of_attendes
print 'Number of audience: '
#theatre.number_of_audience = gets.chomp.to_i
end
def seek_ticket_price
print 'Ticket price: '
#theatre.ticket_price = gets.chomp.to_i
end
def print_revenue
puts "Revenue for the theatre is RM #{#theatre.revenue}."
end
def print_profit
message_prefix = #theatre.profit? ? 'Profit made' : 'Loss incurred'
puts "#{message_prefix} #{#theatre.net.abs}."
end
def self.run
TheatreCLI.new.instance_eval do
seek_ticket_price
seek_number_of_attendes
print_revenue
print_profit
end
end
end
TheatreCLI.run
Notes:
Never use your constructor (initialize method) for anything other than initial setup.
Try to keep all methods under 5 lines.
Always try to keep each class handle a single responsibility; for instance, printing and formatting output is not something the Theatre class needs to care.
Try extracting all hard coded values; eg see the COST hash.
Use apt variables consistent to the domain. Eg: net instead of profit makes the intent clear.

Trying to figure out why Ruby is throwing error on some basic classes & methods

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>'
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
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

Resources