Ruby Training Notes

Russell Bateman
August 2013
last update:

I'm not super-serious about learning Ruby at this point, but my ability to Chef stands greatly impaired until I do learn enough of this language. Here, I'm using Learn Ruby the Hard Way (2011). What's below begins here, using the "Free book online". At least at first, I'm going to use vim and/or gvim under a bash console on Linux Mint 'cause that's how I roll.

The real reason I'm learning Ruby is because I've been using Chef for sometime and not knowing Ruby well enough has begun sticking in my craw.

Please see Ruby Notes: Links for my obligatory "useful links" section.

The ruby subdirectory featured in these notes should be available for download from here.

Exercise 0: The Setup

What's important to observe is that Ruby is installed and usable, that it has a version, and that an interactive Ruby shell, irb, is available for use in performing quick operations. This will be very useful for trying stuff out without having to create a file and execute it.

~/dev/ruby $ ruby -v
ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-linux]
~/dev/ruby $ which irb
/usr/bin/irb
~/dev/ruby $ irb
irb(main):001:0> ^C

Important note

You'll be asked to do some things that presuppose the installation of Ruby's development package. You will not be told this and when you go to do them, they will simply fail. For help in installing the necessary, please see Appendix: Installing Ruby developer software.


Exercise 1: A Good First Program

Occasionally, I throw in some experiments such as ruby ex to see if Ruby's smart-enough to intuit the ubiquitous extension. Apparently, it is not.

The first real thing to learn is how to print stuff out to the console using the puts method.

~/dev/ruby $ ruby ex1.rb

Hello World!
Hello Again
I like typing this.
This is fun.
Yay! Printing.
I'd much rather you 'not'.
I "said" do not touch this.
		
 

ex1.rb:

puts "Hello World!"
puts "Hello Again"
puts "I like typing this."
puts "This is fun."
puts 'Yay! Printing.'
puts "I'd much rather you 'not'."
puts 'I "said" do not touch this.'
		

Exercise 2: Comments and Pound Characters

~/dev/ruby $ ruby ex2.rb

I could have code like this.
This will run.
		
 

ex2.rb:

# A comment, this is so you can read your program later.
# Anything after the # is ignored by Ruby.

puts "I could have code like this." # and the comment after is ignored

# You can also use a comment to "disable" or comment out a piece of code:
# puts "This won't run."

puts "This will run."
		

Exercise 3: Numbers and Math

Operators

+ plus
- minus
/ slash
* asterisk
% percent
< less-than
> greater-than
<= less-than-equal
>= greater-than-equal


There are two things we learn here. First, that numeric operations are just as we'd expect. Second, that after printing text out, we can add a comma and an expression to see its value printed out to the console. Notice, infuriatingly, that the comma forces a newline. If we try to thwart this, nastiness occurs:

~/dev/ruby $ irb
irb(main):001:0> puts "Is it less or equal?", 5 <= -2
Is it less or equal?
false
=> nil
irb(main):002:0> puts "Is it less or equal?" 5 <= -2
SyntaxError: (irb):2: syntax error, unexpected tINTEGER, expecting $end
puts "Is it less or equal?" 5 <= -2
                             ^
	from /usr/bin/irb:12:in `<main>'

~/dev/ruby $ ruby ex3.rb

I will now count my chickens:
Hens
30
Roosters
97
Now I will count the eggs:
7
Is it true that 3 + 2 < 5 - 7?
false
What is 3 + 2?
5
What is 5 - 7?
-2
Oh, that's why it's false.
How about some more.
Is it greater?
true
Is it greater or equal?
true
Is it less or equal?
false
		
 

ex3.rb:

puts "I will now count my chickens:"

puts "Hens", 25 + 30 / 6
puts "Roosters", 100 - 25 * 3 % 4

puts "Now I will count the eggs:"

puts 3 + 2 + 1 - 5 + 4 % 2 - 1 / 4 + 6

puts "Is it true that 3 + 2 < 5 - 7?"

puts 3 + 2 < 5 - 7

puts "What is 3 + 2?", 3 + 2
puts "What is 5 - 7?", 5 - 7

puts "Oh, that's why it's false."

puts "How about some more."

puts "Is it greater?", 5 > -2
puts "Is it greater or equal?", 5 >= -2
puts "Is it less or equal?", 5 <= -2
		

Exercise 4: Variables and Names

The main lesson here is that a Ruby variable is a simple identifier as in most other languages. It's used in expressions, statements, etc. too. Cleverly though, print statements can consume it as an expression similar to the bash shell, ant, etc. using the pattern:

#{variable}

~/dev/ruby $ ruby ex4.rb

There are 100 cars available.
There are only 30 drivers available.
There will be 70 empty cars today.
We can transport 120.0 people today.
We have 90 passengers to carpool today.
We need to put about 3 in each car.
		
 

ex4.rb:

cars = 100
space_in_a_car = 4.0
drivers = 30
passengers = 90
cars_not_driven = cars - drivers
cars_driven = drivers
carpool_capacity = cars_driven * space_in_a_car
average_passengers_per_car = passengers / cars_driven

puts "There are #{cars} cars available."
puts "There are only #{drivers} drivers available."
puts "There will be #{cars_not_driven} empty cars today."
puts "We can transport #{carpool_capacity} people today."
puts "We have #{passengers} passengers to carpool today."
puts "We need to put about #{average_passengers_per_car} in each car."

Exercise 5: More Variables and Printing

Now we're starting to cook with gas when it comes to console output. We learn how to keep the value of a variable getting printed out on the same line and not require a comma. The way this is done looks a lot like C stdio.h. The following work:

~/dev/ruby $ irb
irb(main):001:0> name=Russ
irb(main):002:0> puts "My name is %s." % name
My name is Russ.
=> nil

~/dev/ruby $ ruby ex5.rb

Let's talk about Zed A. Shaw.
He's 74 inches tall.
He's 180 pounds heavy.
Actually that's not too heavy.
He's got Blue eyes and Brown hair.
His teeth are usually White depending on the coffee.
If I add 35, 74, and 180 I get 289.
		
 

ex5.rb:

my_name = 'Zed A. Shaw'
my_age = 35 # not a lie
my_height = 74 # inches
my_weight = 180 # lbs
my_eyes = 'Blue'
my_teeth = 'White'
my_hair = 'Brown'

puts "Let's talk about %s." % my_name
puts "He's %d inches tall." % my_height
puts "He's %d pounds heavy." % my_weight
puts "Actually that's not too heavy."
puts "He's got %s eyes and %s hair." % [my_eyes, my_hair]
puts "His teeth are usually %s depending on the coffee." % my_teeth

# this line is tricky, try to get it exactly right
puts "If I add %d, %d, and %d I get %d." % [
    my_age, my_height, my_weight, my_age + my_height + my_weight]
		

Exercise 6: Strings and Text

Here's where strings are formally introduced. Also introduced is the use of #{variable} in place of % variable which is a more convenient and macro or shell-like way of doing it. Also shown is Ruby string concatenation.

~/dev/ruby $ ruby ex6.rb

There are 10 types of people.
Those who know binary and those who don't.
I said: There are 10 types of people..
I also said: 'Those who know binary and those who don't.'.
Isn't that joke so funny?! false
This is the left side of...a string with a right side.
		
 

ex6.rb:

x = "There are #{10} types of people."
binary = "binary"
do_not = "don't"
y = "Those who know #{binary} and those who #{do_not}."

puts x
puts y

puts "I said: #{x}."
puts "I also said: '#{y}'."

hilarious = false
joke_evaluation = "Isn't that joke so funny?! #{hilarious}"

puts joke_evaluation

w = "This is the left side of..."
e = "a string with a right side."

puts w + e
		

Exercise 7: More Printing

You are meant to notice a number of things that haven't been explicitly introduced like using a conversion specifier to plug in a string literal in the same way a string variable was used previousy and "multiplying" output (puts "." * 10). Also, you're encourged to examine the use of the puts method to kick a newline.

~/dev/ruby $ ruby ex7.rb

Mary had a little lamb.
Its fleece was white as snow.
And everywhere that Mary went.
..........
CheeseBurger
		
 

ex7.rb:

puts "Mary had a little lamb."
puts "Its fleece was white as %s." % 'snow'
puts "And everywhere that Mary went."
puts "." * 10  # what'd that do?

end1 = "C"
end2 = "h"
end3 = "e"
end4 = "e"
end5 = "s"
end6 = "e"
end7 = "B"
end8 = "u"
end9 = "r"
end10 = "g"
end11 = "e"
end12 = "r"

# notice how we are using print instead of puts here. change it to puts
# and see what happens.
print end1 + end2 + end3 + end4 + end5 + end6
print end7 + end8 + end9 + end10 + end11 + end12

# this just is polite use of the terminal, try removing it
puts
		

Exercise 8: Printing, Printing

Now we learn about printing arrays of things, numbers, strings, Booleans and strings.

~/dev/ruby $ ruby ex8.rb

1 2 3 4
one two three four
true false false true
%s %s %s %s %s %s %s %s %s %s %s %s %s %s %s %s
I had this thing. That you could type up right. But it didn't sing. So I said goodnight.
		
 

ex8.rb:

formatter = "%s %s %s %s"

puts formatter % [1, 2, 3, 4]
puts formatter % ["one", "two", "three", "four"]
puts formatter % [true, false, false, true]
puts formatter % [formatter, formatter, formatter, formatter]
puts formatter % [
    "I had this thing.",
    "That you could type up right.",
    "But it didn't sing.",
    "So I said goodnight."
]
		

Exercise 9: Printing, Printing, Printing

Now observe the artificial insertion of newlines into a string. Observe also the special PARAGRAPH-delimiting operator and the redirection operator (<<) used to "redirect" the paragraph to the puts method.

In fact, PARAGRAPH isn't exactly an operator since it could be anything as long as it's an identifier and completely brackets what's to go in it. We'll see that coming up in Exercise 10.

This construct is called a "here document". It's used to build strings from multiple lines. PARAGRAPH is only a made-up delimiter of no other significance.

~/dev/ruby $ ruby ex9.rb

Here are the days:
Mon Tue Wed Thu Fri Sat Sun
Here are the months:
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
There's something going on here.
With the PARAGRAPH thing
We'll be able to type as much as we like.
Even 4 lines if we want, or 5, or 6.
		
 

ex9.rb:

# Here's some new strange stuff, remember type it exactly.

days = "Mon Tue Wed Thu Fri Sat Sun"
months = "Jan\nFeb\nMar\nApr\nMay\nJun\nJul\nAug"

puts "Here are the days: ", days
puts "Here are the months: ", months

puts <<PARAGRAPH
There's something going on here.
With the PARAGRAPH thing
We'll be able to type as much as we like.
Even 4 lines if we want, or 5, or 6.
PARAGRAPH
		

Exercise 10: What Was That? (escape sequences)

This is mostly about escaping various characters like quotes that have important meaning in Ruby code into strings. And, there are other escape sequences too, like \t and probably everything you'd see in the C programming language for reference.

Remember PARAGRAPH? Well here we see there's nothing special about that string of characters.

~/dev/ruby $ ruby ex10.rb

	I'm tabbed in.
I'm split
on a line.
I'm \ a \ cat.
I'll do a list:
	* Cat food
	* Fishies
	* Catnip
	* Grass
		
 

ex10.rb:

tabby_cat = "\tI'm tabbed in."
persian_cat = "I'm split\non a line."
backslash_cat = "I'm \\ a \\ cat."

fat_cat = <<MY_HEREDOC
I'll do a list:
\t* Cat food
\t* Fishies
\t* Catnip\n\t* Grass
MY_HEREDOC

puts tabby_cat
puts persian_cat
puts backslash_cat
puts fat_cat
		

Exercise 11: Asking Questions

Ruby's gets method, its name perhaps inspired (just as puts) by the C Standard Library functions, fetches a string from stdin. The exercises will more difficult to document from here on since they'll require interaction. This is, after all, the beginning of script-writing.

The chomp() method truncates while strip() trims.

We are careful to answer the questions exactly as done in the tutorial.

~/dev/ruby $ ruby ex11.rb

How old are you? 35
How tall are you? 6'2"
How much do you weigh? 180lbs
So, you're 35 years old, 6'2" tall and 180lbs heavy.
		
 

ex11.rb:

print "How old are you? "
age = gets.chomp()
print "How tall are you? "
height = gets.chomp()
print "How much do you weigh? "
weight = gets.chomp()

puts "So, you're #{age} years old, #{height} tall and #{weight} heavy."
		

Exercise 12: Libraries

We step away from what's really useful in script-writing for a moment to observe this topic. I immediately get an error and it's useful to admit that and find a solution.

~/dev/ruby $ ruby ex12.rb

/usr/lib/ruby/1.9.1/net/http.rb:762:in `initialize': getaddrinfo: \
    Name or service not known (SocketError)
	from /usr/lib/ruby/1.9.1/net/http.rb:762:in `open'
	from /usr/lib/ruby/1.9.1/net/http.rb:762:in `block in connect'
	from /usr/lib/ruby/1.9.1/timeout.rb:54:in `timeout'
	from /usr/lib/ruby/1.9.1/timeout.rb:99:in `timeout'
	from /usr/lib/ruby/1.9.1/net/http.rb:762:in `connect'
	from /usr/lib/ruby/1.9.1/net/http.rb:755:in `do_start'
	from /usr/lib/ruby/1.9.1/net/http.rb:744:in `start'
	from /usr/lib/ruby/1.9.1/open-uri.rb:306:in `open_http'
	from /usr/lib/ruby/1.9.1/open-uri.rb:775:in `buffer_open'
	from /usr/lib/ruby/1.9.1/open-uri.rb:203:in `block in open_loop'
	from /usr/lib/ruby/1.9.1/open-uri.rb:201:in `catch'
	from /usr/lib/ruby/1.9.1/open-uri.rb:201:in `open_loop'
	from /usr/lib/ruby/1.9.1/open-uri.rb:146:in `open_uri'
	from /usr/lib/ruby/1.9.1/open-uri.rb:677:in `open'
	from /usr/lib/ruby/1.9.1/open-uri.rb:33:in `open'
	from ex12.rb:3:in `
'
 

ex12.rb:

require 'open-uri'

open("http://www.ruby-lang.org/en") do |f|
  f.each_line {|line| p line}
  puts f.base_uri         # <URI::HTTP:0x40e6ef2 URL:http://www.ruby-lang.org/en/>
  puts f.content_type     # "text/html"
  puts f.charset          # "iso-8859-1"
  puts f.content_encoding # []
  puts f.last_modified    # Thu Dec 05 02:45:02 UTC 2002
end
		

What went wrong? Well, I went to a browser to see if the URL was good in the first place. It was. Then I wondered how Ruby solved problems of proxy since I am behind a firewall. That was it. Here's what I did to make it work.

~/dev/ruby $ export http_proxy="http://web-proxy.austin.hp.com:8080"
~/dev/ruby $ ruby ex12.rb
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"\n"
"   \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">\n"
"<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
"  <head>\n"
"    <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\" />\n"
.
.
.

Exercise 13: Parameters, Unpacking, Variables

Ruby is, above all, a scripting language (and less of a formal language in which to accomplish software engineering). Gathering arguments to a script (a Ruby file) is like bash, for instance, but potentially less cryptic as seen in this code. Instead of ${0} or $0 in Bourne/bash, the name of the script is #{$0}—I guess that's pretty cryptic, but somewhat infrequent. There are doubtless other ways to do this, but we're at the beginning of our Ruby apprentissage.

This stuff is really important for writing utility scripts, something we began in earnest back in lesson 11. The next lesson will be even more useful to this effort.

~/dev/ruby $ ruby ex13.rb first 2nd 3rd
~/dev/ruby $ ruby ex13.rb cheese apples bread
~/dev/ruby $ ruby ex13.rb Zed A. Shaw
~/dev/ruby $ ruby ex13.rb

The script is called: ex13.rb
Your first variable is: first
Your second variable is: 2nd
Your third variable is: 3rd

The script is called: ex13.rb
Your first variable is: cheese
Your second variable is: apples
Your third variable is: bread

The script is called: ex13.rb
Your first variable is: Zed
Your second variable is: A.
Your third variable is: Shaw

The script is called: ex13.rb
Your first variable is:
Your second variable is:
Your third variable is:
		
 

ex13.rb:



first, second, third = ARGV

puts "The script is called: #{$0}"
puts "Your first variable is: #{first}"
puts "Your second variable is: #{second}"
puts "Your third variable is: #{third}"
		

Exercise 14: Prompting and Passing

Now we get oozingly involved in utility script-writing.

Mostly, this is to show how you must always use the chomp() method when writing interactive scripts because otherwise, you'll be including newlines in the answeres which will be at very least annoying.

Observe the use of MESSAGE. It's like PARAGRAPH and MY_HEREDOC we've already seen. These are not reserved keywords in Ruby.

ARGV

It's important to realize that "simple" gets fetches stuff from the command line until all arguments have been consumed. If your caller added fantasy arguments, you would be getting them instead of asking the user to type in the answers to questions shown here. So, use STDIN.gets to avoid that.

Actually, this is a pretty cool thing: How many times have you written an interactive shell script, in which you had to ask questions and get answers, but wished you could be more at leisure to intermingle command-line arguments and interactive answers more naturally?

~/dev/ruby $ ruby ex14.rb Zed

Hi Zed, I'm the ex14.rb script.
I'd like to ask you a few questions.
Do you like me Zed?
> Yes, I wrote you
Where do you live Zed?
> The Rocky Mountains
What kind of computer do you have?
> I built it myself
Alright, so you said Yes, I wrote you about liking me.
You live in The Rocky Mountains.  Not sure where that is.
And you have a I built it myself computer.  Nice.
		
 

ex14.rb:

user = ARGV.first
prompt = '> '

puts "Hi #{user}, I'm the #{$0} script."
puts "I'd like to ask you a few questions."
puts "Do you like me #{user}?"
print prompt
likes = STDIN.gets.chomp()

puts "Where do you live #{user}?"
print prompt
lives = STDIN.gets.chomp()

puts "What kind of computer do you have?"
print prompt
computer = STDIN.gets.chomp()

puts <<MESSAGE
Alright, so you said #{likes} about liking me.
You live in #{lives}.  Not sure where that is.
And you have a #{computer} computer.  Nice.
MESSAGE
		

Exercise 15: Reading Files

What was learned in the previous lesson was to prepare us for beginning to read files. An important construct is ARGV and STDIN, which we already learned. A new construct is the object, File.

Pay close attention to the rather complex, but very flexible way to handle prompting and input using puts, print and STDIN.gets.

~/dev/ruby $ ruby ex15.rb ex15_sample.txt

Here's your file: ex15_sample.txt
This is stuff I typed into a file.
It is really cool stuff.
Lots and lots of fun to have in here.
I'll also ask you to type it again:
> ex15_sample.txt
This is stuff I typed into a file.
It is really cool stuff.
Lots and lots of fun to have in here.
		
 

ex15.rb:

filename = ARGV.first

prompt = "> "
txt = File.open(filename)

puts "Here's your file: #{filename}"
puts txt.read()
close()

puts "I'll also ask you to type it again:"
print prompt
file_again = STDIN.gets.chomp()

txt_again = File.open(file_again)

puts txt_again.read()
again.close()
		

File ex15_sample.txt:

This is stuff I typed into a file.
It is really cool stuff.
Lots and lots of fun to have in here.
		

Notice that I added the close() calls as suggested.

More stuff to learn...

It appeared we were to learn about ri, a sort of on-line documentor for Ruby. This does not work. Even assuming it needs a proxy was insufficient to get it to work. Please see the Appendix to get this working.

Exercise 16: Reading and Writing Files

Ruby file I/O
open( filename, action ) Opens filename for action {r|w|a}.
close() Closes the file.
read() Reads the file's contents.
readline() Reads a single line from file.
truncate() Empties the open file.
write() Writes to the file if opened with w.
exists Returns true/false that the file exists.

~/dev/ruby $ touch test.txt
~/dev/ruby $ ruby ex16.rb test.txt

We're going to erase test.txt.
If you don't want that, hit CTRL-C (^C).
If you do want that, hit RETURN.
?
Opening the file...
Truncating the file.  Goodbye!
Now I'm going to ask you for three lines.
line 1: To all the people out there.
line 2: I say I don't like my hair.
line 3: I need to shave it off.
I'm going to write these to the file.
And finally, we close it.
		

~/dev/ruby $ cat test.txt

To all the people out there.
I say I don't like my hair.
I need to shave it off.
		
 

ex16.rb:

filename = ARGV.first
script = $0

puts "We're going to erase #{filename}."
puts "If you don't want that, hit CTRL-C (^C)."
puts "If you do want that, hit RETURN."

print "? "
STDIN.gets

puts "Opening the file..."
target = File.open(filename, 'w')

puts "Truncating the file.  Goodbye!"
target.truncate(target.size)

puts "Now I'm going to ask you for three lines."

print "line 1: "; line1 = STDIN.gets.chomp()
print "line 2: "; line2 = STDIN.gets.chomp()
print "line 3: "; line3 = STDIN.gets.chomp()

puts "I'm going to write these to the file."

target.write(line1)
target.write("\n")
target.write(line2)
target.write("\n")
target.write(line3)
target.write("\n")

puts "And finally, we close it."
target.close()
		

Exercise 17: More Files

We learn to read one file and copy it to another. We learn about File.exists and getting a string's length using the length method. Note that if output.close() isn't called, the file will not exist just as happens with any other file I/O library in any other language.

~/dev/ruby $ ruby ex17.rb test.txt copied.txt

Copying from test.txt to copied.txt
The input file is 81 bytes long
Does the output file exist? false
Ready, hit RETURN to continue, CTRL-C to abort.

Alright, all done.
		

~/dev/ruby $ cat copied.txt

To all the people out there.
I say I don't like my hair.
I need to shave it off.
		
 

ex17.rb:

from_file, to_file = ARGV
script = $0

puts "Copying from #{from_file} to #{to_file}"

# we could do these two on one line too, how?
input = File.open(from_file)
indata = input.read()

puts "The input file is #{indata.length} bytes long"

puts "Does the output file exist? #{File.exists? to_file}"
puts "Ready, hit RETURN to continue, CTRL-C to abort."
STDIN.gets

output = File.open(to_file, 'w')
output.write(indata)

puts "Alright, all done."

output.close()
input.close()
		

Exercise 18: Names, Variables, Code, Functions

...in Ruby. I have learned to say "method" under influence of Java, but Ruby was born concurrently with Java, so likely "function" is more correct and under the influence of the C programming language. It is said that "every Ruby function is a method and that methods are always called on objects". So, I'll retain the nomenclature of "method".

The "extra credit" section of this exercise is worth reading for the cultural comments about functions (or "methods").

~/dev/ruby $ ruby ex18.rb

arg1: Zed, arg2: Shaw
arg1: Zed, arg2: Shaw
arg1: First!
I got nothin'.
		
 

ex18.rb:

# this one is like your scripts with argv
def puts_two(*args)
  arg1, arg2 = args
  puts "arg1: #{arg1}, arg2: #{arg2}"
end

# ok, that *args is actually pointless, we can just do this
def puts_two_again(arg1, arg2)
  puts "arg1: #{arg1}, arg2: #{arg2}"
end

# this just takes one argument
def puts_one(arg1)
  puts "arg1: #{arg1}"
end

# this one takes no arguments
def puts_none()
  puts "I got nothin'."
end

puts_two("Zed","Shaw")
puts_two_again("Zed","Shaw")
puts_one("First!")
puts_none()
		

Exercise 19: Functions and Variables

This exercise is mostly a waste of time if you've coded in some scripting languages which do not respect scope and real programming languages that do.

~/dev/ruby $ ruby ex19.rb

We can just give the function numbers directly:
You have 20 cheeses!
You have 30 boxes of crackers!
Man that's enough for a party!
Get a blanket.

OR, we can use variables from our script:
You have 10 cheeses!
You have 50 boxes of crackers!
Man that's enough for a party!
Get a blanket.

We can even do math inside too:
You have 30 cheeses!
You have 11 boxes of crackers!
Man that's enough for a party!
Get a blanket.

And we can combine the two, variables and math:
You have 110 cheeses!
You have 1050 boxes of crackers!
Man that's enough for a party!
Get a blanket.
		
 

ex19.rb:

def cheese_and_crackers(cheese_count, boxes_of_crackers)
  puts "You have #{cheese_count} cheeses!"
  puts "You have #{boxes_of_crackers} boxes of crackers!"
  puts "Man that's enough for a party!"
  puts "Get a blanket."
  puts # a blank line
end

puts "We can just give the function numbers directly:"
cheese_and_crackers(20, 30)

puts "OR, we can use variables from our script:"
amount_of_cheese = 10
amount_of_crackers = 50
cheese_and_crackers(amount_of_cheese, amount_of_crackers)

puts "We can even do math inside too:"
cheese_and_crackers(10 + 20, 5 + 6)

puts "And we can combine the two, variables and math:"
cheese_and_crackers(amount_of_cheese + 100, amount_of_crackers + 1000)
		

Exercise 20: Functions and Files

~/dev/ruby $ ruby ex20.rb

First let's print the whole file:

To all the people out there.
I say I don't like my hair.
I need to shave it off.
Now let's rewind, kind of like a tape.
Let's print three lines:
1 To all the people out there.
2 I say I don't like my hair.
3 I need to shave it off.
		
 

ex20.rb:

input_file = ARGV[0]

def print_all(f)
  puts f.read()
end

def rewind(f)
  f.seek(0, IO::SEEK_SET)
end

def print_a_line(line_count, f)
  puts "#{line_count} #{f.readline()}"
end

current_file = File.open(input_file)

puts "First let's print the whole file:"
puts # a blank line

print_all(current_file)

puts "Now let's rewind, kind of like a tape."

rewind(current_file)

puts "Let's print three lines:"

current_line = 1
print_a_line(current_line, current_file)

current_line = current_line + 1
print_a_line(current_line, current_file)

current_line = current_line + 1
print_a_line(current_line, current_file)
		

Create trouble...

Just for grins, let's execute this script without arguments. It's important to begin learning how to debug problems.

ex20.rb:15:in `initialize': can't convert nil into String (TypeError)
	from ex20.rb:15:in `open'
	from ex20.rb:15:in `
'

There's a genuine problem with this script, however, in that no blank line is printed after line 18, which the exercise shows, but didn't happen locally. "My kingdom for a debugger," as they say.

Exercise 21: Functions Can Return Something

Mostly, this just demonstrates to the surprised that Ruby methods can return values for use by a caller. Again, no surprises.

~/dev/ruby $ ruby ex21.rb

Let's do some math with just functions!
ADDING 30 + 5
SUBTRACTING 78 - 4
MULTIPLYING 90 * 2
DIVIDING 100 / 2
Age: 35, Height: 74, Weight: 180, IQ: 50
Here is a puzzle.
DIVIDING 50 / 2
MULTIPLYING 180 * 25
SUBTRACTING 74 - 4500
ADDING 35 + -4426
That becomes: -4391 Can you do it by hand?
		
 

ex21.rb:

def add(a, b)
  puts "ADDING #{a} + #{b}"
  a + b
end

def subtract(a, b)
  puts "SUBTRACTING #{a} - #{b}"
  a - b
end

def multiply(a, b)
  puts "MULTIPLYING #{a} * #{b}"
  a * b
end

def divide(a, b)
  puts "DIVIDING #{a} / #{b}"
  a / b
end

puts "Let's do some math with just functions!"

age = add(30, 5)
height = subtract(78,4)
weight = multiply(90, 2)
iq = divide(100, 2)

puts "Age: #{age}, Height: #{height}, Weight: #{weight}, IQ: #{iq}"

# A puzzle for the extra credit, type it in anyway.
puts "Here is a puzzle."

what = add(age, subtract(height, multiply(weight, divide(iq, 2))))

puts "That becomes: #{what} Can you do it by hand?"
		

Exercise 22: What Do You Know So Far?

This is a non-coding assignment. I found the following aids...

...and kept adding to them as a "useful links" section here.

Exercise 23: Read Some Code

As I'm not in this for the hoop-jumping (though I greatly appreciate the approach), and this is another non-coding assignment, I'm not going to write anything up here. My Chef recipes, the ones I'm trying to understand and modify, are enough to satisfy this.


Exercise 24: More Practice (escape sequences)

This exercise is getting close to winding up the first half and is only review of what's gone before.

~/dev/ruby $ ruby ex24.rb

Let's practice everything.
You'd need to know 'bout escapes with \ that do
 newlines and 	 tabs.
--------------

	The lovely world
with logic so firmly planted
cannot discern
 the needs of love
nor comprehend passion from intuition
and requires an explanation

		where there is none.

--------------
This should be five: 5
With a starting point of: 10000
We'd have 5000000 beans, 5000 jars, and 50 crates.
We can also do that this way:
We'd have 500000 beans, 500 jars, and 5 crates.
		
 

ex24.rb:

puts "Let's practice everything."
puts "You\'d need to know \'bout escapes with \\ that do \n newlines and \t tabs."

poem = <<MULTI_LINE_STRING

\tThe lovely world
with logic so firmly planted
cannot discern \n the needs of love
nor comprehend passion from intuition
and requires an explanation
\n\t\twhere there is none.

MULTI_LINE_STRING

puts "--------------"
puts poem
puts "--------------"

five = 10 - 2 + 3 - 6
puts "This should be five: #{five}"

def secret_formula(started)
  jelly_beans = started * 500
  jars = jelly_beans / 1000
  crates = jars / 100
  return jelly_beans, jars, crates
end

start_point = 10000
beans, jars, crates = secret_formula(start_point)

puts "With a starting point of: #{start_point}"
puts "We'd have #{beans} beans, #{jars} jars, and #{crates} crates."

start_point = start_point / 10

puts "We can also do that this way:"
puts "We'd have %s beans, %s jars, and %s crates." % secret_formula(start_point)
		

Exercise 25: Even More Practice (functions and variables)

Just functions. Initially, this is run only to guarantee there are no syntax errors. Then, a freer-form exercise in irb is worked through. That's actually pretty cool because it reintroduces Ruby require and then makes use of the methods/functions defined.

It's Friday evening and I'm in a hurry to blow this off, so I'm not going to record the interactive exercise. Okay, I lied; I'm going to do it here...

~/dev/ruby $ ruby ex25.rb

(nothing)
		

~/dev/ruby $ irb

irb(main):001:0> require './ex25.rb'
=> true
irb(main):002:0> sentence = "All good things come to those who wait."
=> "All good things come to those who wait."
irb(main):003:0> words = Ex25.break_words( sentence )
=> ["All", "good", "things", "come", "to", "those", "who", "wait."]
irb(main):004:0> sorted_words = Ex25.sort_words( words )
=> ["All", "come", "good", "things", "those", "to", "wait.", "who"]
irb(main):005:0> Ex25.print_first_word( words )
All
=> nil
irb(main):006:0> Ex25.print_last_word( words )
wait.
=> nil
irb(main):007:0> Ex25.wrods
NoMethodError: undefined method `wrods' for Ex25:Module
	from (irb):7
	from /usr/bin/irb:12:in `<main>'
irb(main):008:0> words
=> ["good", "things", "come", "to", "those", "who"]
irb(main):009:0> Ex25.print_first_word( sorted_words )
All
=> nil
irb(main):010:0> Ex25.print_last_word( sorted_words )
who
=> nil
irb(main):011:0> sorted_words
=> ["come", "good", "things", "those", "to", "wait."]
irb(main):012:0> Ex25.sort_sentence( sentence )
=> ["All", "come", "good", "things", "those", "to", "wait.", "who"]
irb(main):013:0> Ex25.print_first_and_last( sentence )
All
wait.
=> nil
irb(main):014:0> Ex25.print_first_and_last_sorted( sentence )
All
who
=> nil
irb(main):015:0>
		
 

ex25.rb:

module Ex25
  def self.break_words(stuff)
    # This function will break up words for us.
    words = stuff.split(' ')
    words
  end

  def self.sort_words(words)
    # Sorts the words.
    words.sort()
  end

  def self.print_first_word(words)
    # Prints the first word and shifts the others down by one.
    word = words.shift()
    puts word
  end

  def self.print_last_word(words)
    # Prints the last word after popping it off the end.
    word = words.pop()
    puts word
  end

  def self.sort_sentence(sentence)
    # Takes in a full sentence and returns the sorted words.
    words = break_words(sentence)
    sort_words(words)
  end

  def self.print_first_and_last(sentence)
    # Prints the first and last words of the sentence.
    words = break_words(sentence)
    print_first_word(words)
    print_last_word(words)
  end

  def self.print_first_and_last_sorted(sentence)
    # Sorts the words then prints the first and last one.
    words = sort_sentence(sentence)
    print_first_word(words)
    print_last_word(words)
  end
end
		

Exercise 26: Congratulations, Take a Test!

This exercise is sort of halfway through the tutorial with much more, better and harder stuff promised.

The link to the test code is http://ruby.learncodethehardway.org/book/exercise26.txt. The test is to comb through this source code in search of and to fix errors.

~/dev/ruby $ ruby ex26.rb (first go, let's get started...)

 ex26.rb:21: syntax error, unexpected tIDENTIFIER, expecting ')'
    puts word
        ^
ex26.rb:67: syntax error, unexpected $undefined, expecting keyword_end
    jars = jelly_beans \ 1000
                        ^
ex26.rb:73: syntax error, unexpected tEQ, expecting '='
beans, jars, crates == secret_formula(start-point)
                      ^
ex26.rb:76: syntax error, unexpected ')', expecting '='
ex26.rb:84: syntax error, unexpected tIDENTIFIER, expecting ')'
sentence = "All god\tthings come to those who weight."
        ^
ex26.rb:98: syntax error, unexpected $end, expecting ')'

(errors appearing, one at a time, after fixing the original set above:)
ex26.rb:73:in `
': undefined local variable or method `start' for main:Object (NameError) ex26.rb:81:in `
': undefined local variable or method `start_pont' for main:Object (NameError) ex26.rb:86:in `
': undefined local variable or method `ex25' for main:Object (NameError) ex26.rb:87:in `
': undefined local variable or method `ex25' for main:Object (NameError) ex26.rb:10:in `sort_words': undefined method `sorted' for main:Object (NoMethodError) from ex26.rb:87:in `
' ex26.rb:9:in `sort_words': undefined method `sort' for main:Object (NoMethodError) from ex26.rb:87:in `
' ex26.rb:20:in `pop': negative array size (ArgumentError) from ex26.rb:20:in `puts_last_word' from ex26.rb:90:in `
' ex26.rb:94:in `
': undefined method `prin' for main:Object (NoMethodError) ex26.rb:96:in `
': undefined method `puts_irst_and_last' for main:Object (NoMethodError) ex26.rb:98:in `
': undefined local variable or method `senence' for main:Object (NameError) ex26.rb:98:in `
': undefined method `puts_first_a_last_sorted' for main:Object (NoMethodError)

Note that two of the errors were created by bad syntax from the immediately previous (non-blank) line. The others were simple syntax errors like bad operators, parens for brackets, etc. In the end...

...I was unable to fix all the errors in the time I alloted to this exercise. My conclusion is, in part, don't let someone else write a pile of Ruby crap for you to debug. Instead, either write the crap yourself or write tests for everything you're going to write so that you don't inflict this madness on anyone following you.

Of course, I did fix all the errors. I just wanted to plug test-driven development (TDD).

 

ex26.rb:

# This function will break up words for us.
def break_words(stuff)
    words = stuff.split(' ')
    return words
end

# Sorts the words.
def sort_words(words)
    return sorted(words)
end

# Prints the first word after popping it off.
def puts_first_word(words)
    word = words.poop(0)
    puts word
end

# Prints the last word after popping it off.
def puts_last_word(words)
    word = words.pop(-1
    puts word
end

# Takes in a full sentence and returns the sorted words.
def sort_sentence(sentence)
    words = break_words(sentence)
    return sort_words(words)
end

# Puts the first and last words of the sentence.
def puts_first_and_last(sentence)
    words = break_words(sentence)
    puts_first_word(words)
    puts_last_word(words)
end

# Sorts the words then prints the first and last one.
def puts_first_and_last_sorted(sentence)
    words = sort_sentence(sentence)
    puts_first_word(words)
    puts_last_word(words)
end


puts "Let's practice everything."
puts 'You\'d need to know \'bout escapes with \\ that do \n newlines and \t tabs.'

poem = <<POEM
\tThe lovely world
with logic so firmly planted
cannot discern \n the needs of love
nor comprehend passion from intuition
and requires an explantion
\n\t\twhere there is none.
POEM


puts "--------------"
puts poem
puts "--------------"

five = 10 - 2 + 3 - 5
puts "This should be five: %s" % five

def secret_formula(started)
    jelly_beans = started * 500
    jars = jelly_beans \ 1000
    crates = jars / 100
    return jelly_beans, jars, crates
end

start_point = 10000
beans, jars, crates == secret_formula(start-point)

puts "With a starting point of: %d" % start_point
puts "We'd have %d jeans, %d jars, and %d crates." % (beans, jars, crates)

start_point = start_point / 10

puts "We can also do that this way:"
puts "We'd have %d beans, %d jars, and %d crabapples." % secret_formula(start_pont


sentence = "All god\tthings come to those who weight."

words = ex25.break_words(sentence)
sorted_words = ex25.sort_words(words)

puts_first_word(words)
puts_last_word(words)
.puts_first_word(sorted_words)
puts_last_word(sorted_words)
sorted_words = ex25.sort_sentence(sentence)
prin sorted_words

puts_irst_and_last(sentence)

puts_first_a_last_sorted(senence)
		

Exercise 27: Memorizing Logic

The relational operators and other elements of logic in Ruby are:

No coded exercise. (Remember, this tutorial is trying to teach non programmers to code.)

Exercise 28: Boolean Practice

More non-coding exercises on the elements of logic in Ruby.

Exercise 29: What If

~/dev/ruby $ ruby ex29.rb

Too many cats! The world is doomed!
The world is dry!
People are greater than or equal to dogs.
People are less than or equal to dogs.
People are dogs.
My parens work.   
		
 

ex29.rb:

people = 20
cats = 30
dogs = 15

if people < cats
  puts "Too many cats! The world is doomed!"
end

if people > cats
  puts "Not many cats! The world is saved!"
end

if people < dogs
  puts "The world is drooled on!"
end

if people > dogs
  puts "The world is dry!"
end

dogs += 5

if people >= dogs
  puts "People are greater than or equal to dogs."
end

if people <= dogs
  puts "People are less than or equal to dogs."
end

if people == dogs
  puts "People are dogs."
end

if( people == dogs )             added this just to see if it works.)
  puts "My parens work."
end
		

Exercise 30: Else and If

~/dev/ruby $ ruby ex30.rb

We should take the cars.
Maybe we could take the buses.
Alright, let's just take the buses.
		
 

ex30.rb:

people = 30
cars = 40
buses = 15

if cars > people
  puts "We should take the cars."
elsif cars < people
  puts "We should not take the cars."
else
  puts "We can't decide."
end

if buses > cars
  puts "That's too many buses."
elsif buses < cars
  puts "Maybe we could take the buses."
else
  puts "We still can't decide."
end

if people > buses
  puts "Alright, let's just take the buses."
else
  puts "Fine, let's stay home then."
end
		

Exercise 31: Making Decisions

What's beginning to emerge is the observation that, like Bourne and bash Ruby makes no use of block delimiters such as begin/end or { }.

~/dev/ruby $ ruby ex31.rb

You enter a dark room with two doors.  Do you go through door #1 or door #2?
> 1
There's a giant bear here eating a cheese cake.  What do you do?
1. Take the cake.
2. Scream at the bear.
> 2
The bear eats your legs off.  Good job!
		

~/dev/ruby $ ruby ex31.rb

You enter a dark room with two doors.  Do you go through door #1 or door #2?
> 1
There's a giant bear here eating a cheese cake.  What do you do?
1. Take the cake.
2. Scream at the bear.
> 1
The bear eats your face off.  Good job!
		

~/dev/ruby $ ruby ex31.rb

You enter a dark room with two doors.  Do you go through door #1 or door #2?
> 2
You stare into the endless abyss at Cthuhlu's retina.
1. Blueberries.
2. Yellow jacket clothespins.
3. Understanding revolvers yelling melodies.
> 1
Your body survives powered by a mind of jello.  Good job!
		

~/dev/ruby $ ruby ex31.rb

You enter a dark room with two doors.  Do you go through door #1 or door #2?
> 2
You stare into the endless abyss at Cthuhlu's retina.
1. Blueberries.
2. Yellow jacket clothespins.
3. Understanding revolvers yelling melodies.
> 3
The insanity rots your eyes into a pool of muck.  Good job!
		

~/dev/ruby $ ruby ex31.rb

You enter a dark room with two doors.  Do you go through door #1 or door #2?
> stuff
You stumble around and fall on a knife and die.  Good job!
		

~/dev/ruby $ ruby ex31.rb

You enter a dark room with two doors.  Do you go through door #1 or door #2?
> 1
There's a giant bear here eating a cheese cake.  What do you do?
1. Take the cake.
2. Scream at the bear.
> apples
Well, doing apples is probably better.  Bear runs away.
		
 

ex31.rb:

def prompt
  print "> "
end

puts "You enter a dark room with two doors.  Do you go through door #1 or door #2?"

prompt; door = gets.chomp

if door == "1"
  puts "There's a giant bear here eating a cheese cake.  What do you do?"
  puts "1. Take the cake."
  puts "2. Scream at the bear."

  prompt; bear = gets.chomp

  if bear == "1"
    puts "The bear eats your face off.  Good job!"
  elsif bear == "2"
    puts "The bear eats your legs off.  Good job!"
  else
    puts "Well, doing #{bear} is probably better.  Bear runs away."
  end

elsif door == "2"
  puts "You stare into the endless abyss at Cthuhlu's retina."
  puts "1. Blueberries."
  puts "2. Yellow jacket clothespins."
  puts "3. Understanding revolvers yelling melodies."

  prompt; insanity = gets.chomp

  if insanity == "1" or insanity == "2"
    puts "Your body survives powered by a mind of jello.  Good job!"
  else
    puts "The insanity rots your eyes into a pool of muck.  Good job!"
  end

else
  puts "You stumble around and fall on a knife and die.  Good job!"
end
		

Exercise 32: Loops and Arrays

Again, nothing of the comfort of swimming-pool edges (braces, etc.) to hang onto. This is a scripting language.

Ruby navigation
if if expression   true-part   end
if if expression   true-part   else   false-part   end
if if expression   true-part   elseif expression   true-part   end
for for variable in collection   ...   end
while while expression   ...   end

There are some cool things about arrays in Ruby. Check out Array for methods. There is no lack of functionality.


~/dev/ruby $ ruby ex32.rb

This is count 1
This is count 2
This is count 3
This is count 4
This is count 5
A fruit of type: apples
A fruit of type: oranges
A fruit of type: pears
A fruit of type: apricots
I got 1
I got pennies
I got 2
I got dimes
I got 3
I got quarters
Adding 0 to the list.
Adding 1 to the list.
Adding 2 to the list.
Adding 3 to the list.
Adding 4 to the list.
Adding 5 to the list.
Element was: 0
Element was: 1
Element was: 2
Element was: 3
Element was: 4
Element was: 5
		
 

ex32.rb:

the_count = [1, 2, 3, 4, 5]
fruits = ['apples', 'oranges', 'pears', 'apricots']
change = [1, 'pennies', 2, 'dimes', 3, 'quarters']

# this first kind of for-loop goes through an array
for number in the_count
  puts "This is count #{number}"
end

# same as above, but using a block instead
fruits.each do |fruit|
  puts "A fruit of type: #{fruit}"
end

# also we can go through mixed arrays too
for i in change
  puts "I got #{i}"
end

# we can also build arrays, first start with an empty one
elements = []

# then use a range object to do 0 to 5 counts
for i in (0..5)
  puts "Adding #{i} to the list."
  # push is a function that arrays understand
  elements.push(i)
end

# now we can puts them out too
for i in elements
  puts "Element was: #{i}"
end
		

Exercise 33: While Loops

See syntax in previous exercise's comments.

~/dev/ruby $ ruby ex33.rb

At the top i is 0
Numbers now: [0]
At the bottom i is 1
At the top i is 1
Numbers now: [0, 1]
At the bottom i is 2
At the top i is 2
Numbers now: [0, 1, 2]
At the bottom i is 3
At the top i is 3
Numbers now: [0, 1, 2, 3]
At the bottom i is 4
At the top i is 4
Numbers now: [0, 1, 2, 3, 4]
At the bottom i is 5
At the top i is 5
Numbers now: [0, 1, 2, 3, 4, 5]
At the bottom i is 6
The numbers:
0
1
2
3
4
5
		
 

ex33.rb:

i = 0
numbers = []

while i < 6
  puts "At the top i is #{i}"
  numbers.push(i)

  i = i + 1
  puts "Numbers now: #{numbers}"
  puts "At the bottom i is #{i}"
end

puts "The numbers: "

for num in numbers
  puts num
end
		

Exercise 34: Accessing Elements of Arrays

Arrays can be of most anything. They're defined using square brackets as delimiters. They're subscripted as in other languages, e.g.: array[ 0 ], but I don't show an exercise with that.

animals    = ['bear', 'tiger', 'penguin', 'zebra']
colors     = [ "red", "yellow", "blue", "green" ]
digits     = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
hexdigits  = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' ]
hexnumbers = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f ]

I made up my own exercise just to validate what I posited in the previous paragraph. The tutorial had none.

~/dev/ruby $ ruby ex34.rb

bear
tiger
penguin
zebra
red
yellow
blue
green
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
a
b
c
d
e
f
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
		
 

ex34.rb:

animals    = ['bear', 'tiger', 'penguin', 'zebra']
colors     = [ "red", "yellow", "blue", "green" ]
digits     = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
hexdigits  = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' ]
hexnumbers = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f ]

puts animals
puts colors
puts digits
puts hexdigits
puts hexnumbers
		

Exercise 35: Branches and Functions

There are some emerging standard practices in this script code such as how to get input, e.g.: ans = gets.chomp.

Also, we're introduced to another concept, Ruby's array include? method that returns true/false that some value is an element of (in) the array. Here's some playing around to illustrate that:

~/dev/ruby $ irb
irb(main):001:0> array = [ 'cat', 'dog', 'ant' ]
=> ["cat", "dog", "ant"]
irb(main):002:0> array.include? "dog"
=> true
irb(main):003:0> array = [ 1, 2, 3 ]
=> [1, 2, 3]
irb(main):004:0> array.include?( 3 )
=> true
irb(main):005:0> array.include? 2
=> true

Last, Ruby method to_i returns the leading characters of a string as an integer. With no base (in parens), decimal is assumed, otherwise a base in the range { 2..36 } can be specified.


~/dev/ruby $ ruby ex35.rb

You are in a dark room.
There is a door to your right and left.
Which one do you take?
> left
There is a bear here.
The bear has a bunch of honey.
The fat bear is in front of another door.
How are you going to move the bear?
> taunt bear
The bear has moved from the door. You can go through it now.
> open door
This room is full of gold.  How much do you take?
> asf
Man, learn to type a number.  Good job!
		
 

ex35.rb:

def prompt()
  print "> "
end

def gold_room()
  puts "This room is full of gold.  How much do you take?"

  prompt; next_move = gets.chomp
  if next_move.include? "0" or next_move.include? "1"
    how_much = next_move.to_i()
  else
    dead("Man, learn to type a number.")
  end

  if how_much < 50
    puts "Nice, you're not greedy, you win!"
    Process.exit(0)
  else
    dead("You greedy bastard!")
  end
end


def bear_room()
  puts "There is a bear here."
  puts "The bear has a bunch of honey."
  puts "The fat bear is in front of another door."
  puts "How are you going to move the bear?"
  bear_moved = false

  while true
    prompt; next_move = gets.chomp

    if next_move == "take honey"
      dead("The bear looks at you then slaps your face off.")
    elsif next_move == "taunt bear" and not bear_moved
      puts "The bear has moved from the door. You can go through it now."
      bear_moved = true
    elsif next_move == "taunt bear" and bear_moved
      dead("The bear gets pissed off and chews your leg off.")
    elsif next_move == "open door" and bear_moved
      gold_room()
    else
      puts "I got no idea what that means."
    end
  end
end

def cthulhu_room()
  puts "Here you see the great evil Cthulhu."
  puts "He, it, whatever stares at you and you go insane."
  puts "Do you flee for your life or eat your head?"

  prompt; next_move = gets.chomp

  if next_move.include? "flee"
    start()
  elsif next_move.include? "head"
    dead("Well that was tasty!")
  else
    cthulhu_room()
  end
end

def dead(why)
  puts "#{why}  Good job!"
  Process.exit(0)
end

def start()
  puts "You are in a dark room."
  puts "There is a door to your right and left."
  puts "Which one do you take?"

  prompt; next_move = gets.chomp

  if next_move == "left"
    bear_room()
  elsif next_move == "right"
    cthulhu_room()
  else
    dead("You stumble around the room until you starve.")
  end
end

start()
		

Exercise 36: Designing and Debugging

The author gives some best-practice advice. Some are interesting, some are not and some may not appear interesting now, but may become so with more experience. Some of this advice smells like the absence of TDD. Maybe Ruby can't do that.

if-statements

  1. Every if-statement must have an else.
  2. If this else should never be run because it doesn't make sense, then use a die function in the else that prints out an error message and dies, just as done in the last exercise. This will find many errors.
  3. Never nest if-statements more than 2 deep and always try to do them 1 deep. This means if you put an if in an if then you should be looking to move that second if into a method.
  4. Treat if-statements like paragraphs, where each if, elsif, else grouping is like a set of sentences. Put blank lines before and after.
  5. Your boolean tests should be simple. If they are complex, move their calculations to variables earlier in your function and use a good name for the variable.

for loops

  1. Use a while loop only to loop forever, and that means probably never. This only applies to Ruby, other languages are different.
  2. Use a for loop for all other kinds of looping, especially if there is a fixed or limited number of things to loop over.

Debugging

  1. Do not use a debugger. A debugger is like doing a full-body scan on a sick person. You do not get any specific useful information, and you find a whole lot of information that doesn't help and is just confusing.
  2. The best way to debug a program is to use puts or p to print out the values of variables at points in the program to see where they go wrong.
  3. Make sure parts of your programs work as you work on them. Do not write massive files of code before you try to run them. Code a little, run a little, fix a little.

Exercise 37: Symbol Review

Again, no code exercise, but a list of keywords, datatypes, escape sequences and operators to review.

Exercise 38: Doing Things to Lists

The objective here is to go through the code analyzing an imaginary Ruby translation of it to understand what Ruby's doing for:

mystuff.push( "hello " )

The steps are given in the exercise, but they're sort of a reduced human view. These are (in my own words which are very different from the authors):

  1. Ruby scans mystuff and searches the symbol table to see if it's already defined; is it a scoped argument or global variable?
  2. Ruby encounters the dot operator; as it knows mystuff is an array, it looks to push as being a method.
  3. Ruby scans the argument, "hello", pushes it onto the call stack then invokes the push() method adding the element "hello" to the end of array mystuff.

One is expected to undertake such a description for every identifier in the exercise code below.

~/dev/ruby $ ruby ex38.rb

Wait there's not 10 things in that list, let's fix that.
Adding: Boy
There's 7 items now.
Adding: Girl
There's 8 items now.
Adding: Banana
There's 9 items now.
Adding: Corn
There's 10 items now.
There we go: ["Apples", "Oranges", "Crows", "Telephone", "Light", "Sugar", "Boy", "Girl", "Banana", "Corn"]
Let's do some things with stuff.
Oranges
Corn
Corn
Apples Oranges Crows Telephone Light Sugar Boy Girl Banana
Telephone#Sugar
		
 

ex38.rb:

ten_things = "Apples Oranges Crows Telephone Light Sugar"

puts "Wait there's not 10 things in that list, let's fix that."

stuff = ten_things.split(' ')
more_stuff = %w(Day Night Song Frisbee Corn Banana Girl Boy)

while stuff.length != 10
  next_one = more_stuff.pop()
  puts "Adding: #{next_one}"
  stuff.push(next_one)
  puts "There's #{stuff.length} items now."
end

puts "There we go: #{stuff}"

puts "Let's do some things with stuff."

puts stuff[1]
puts stuff[-1] # whoa! fancy
puts stuff.pop()
puts stuff.join(' ') # what? cool!
puts stuff.values_at(3,5).join('#') # super stellar!
		

Exercise 39: Hashes, Oh Lovely Hashes

In Ruby, a hash is what is more properly termed a map or, if the notion of the underlying indexing strategy is to be included, a hash map. The tutorial author's explanation is somewhat torturous, but it has to be remembered that he's targeting essentially a non-programming audience.

Hashmaps do not have order. If you print one out, it comes out in whatever order they're stored in, which from my observation is, at least at first, the order in which they're built.

This construct is what imparts the greatest amount of obfuscation to reading Ruby when coming from another language. It's the "hash rocket" that does it.

And, suddenly, the use of braces springs into Ruby life for the purpose of hashmap initialization.

~/dev/ruby $ ruby ex39.rb

----------
NY State has:
New York
OR State has:
Portland
----------
Michigan's abbreviation is:
MI
Florida's abbreviation is:
FL
----------
Michigan has:
Detroit
Florida has:
Jacksonville
----------
Oregon is abbreviated OR
Florida is abbreviated FL
California is abbreviated CA
New York is abbreviated NY
Michigan is abbreviated MI
----------
CA has the city San Francisco
MI has the city Detroit
FL has the city Jacksonville
NY has the city New York
OR has the city Portland
----------
Oregon state is abbreviated OR and has city Portland
Florida state is abbreviated FL and has city Jacksonville
California state is abbreviated CA and has city San Francisco
New York state is abbreviated NY and has city New York
Michigan state is abbreviated MI and has city Detroit
----------
Sorry, no Texas.
The city for the state 'TX' is: Does Not Exist
		
 

ex39.rb:

# create a mapping of state to abbreviation
states = {
    'Oregon' => 'OR',
    'Florida' => 'FL',
    'California' => 'CA',
    'New York' => 'NY',
    'Michigan' => 'MI'
}

# create a basic set of states and some cities in them
cities = {
    'CA' => 'San Francisco',
    'MI' => 'Detroit',
    'FL' => 'Jacksonville'
}

# add some more cities
cities['NY'] = 'New York'
cities['OR'] = 'Portland'

# puts out some cities
puts '-' * 10
puts "NY State has: ", cities['NY']
puts "OR State has: ", cities['OR']

# puts some states
puts '-' * 10
puts "Michigan's abbreviation is: ", states['Michigan']
puts "Florida's abbreviation is: ", states['Florida']

# do it by using the state then cities dict
puts '-' * 10
puts "Michigan has: ", cities[states['Michigan']]
puts "Florida has: ", cities[states['Florida']]

# puts every state abbreviation
puts '-' * 10
for state, abbrev in states
    puts "%s is abbreviated %s" % [state, abbrev]
end

# puts every city in state
puts '-' * 10
for abbrev, city in cities
    puts "%s has the city %s" % [abbrev, city]
end

# now do both at the same time
puts '-' * 10
for state, abbrev in states
    puts "%s state is abbreviated %s and has city %s" % [
        state, abbrev, cities[abbrev]]
end

puts '-' * 10
# if it's not there you get nil
state = states['Texas']

if not state
    puts "Sorry, no Texas."
end

# get a city with a default value
city = cities['TX'] || 'Does Not Exist'
puts "The city for the state 'TX' is: %s" % city
		

Exercise 40: Modules, Classes and Objects

Ruby lays claim to object-orientation and so sports the ability to define classes. The author thinks there's something peculiarly unintuitive about object-oriented programming and tippy-toes around calling it "just plain weird." He compares Ruby modules to hashmaps and classes to modules

The tutorial text does contain examples slightly more interesting than the exercise code here. It's best to examine the examples while ignoring the comments about how hard OOP is.

~/dev/ruby $ ruby ex40.rb

Happy birthday to you
I don't want to get sued
So I'll stop right there
They rally around the family
With pockets full of shells
		
 

ex40.rb:

class Song
    def initialize(lyrics)
        @lyrics = lyrics
    end

    def sing_me_a_song()
        for line in @lyrics
            puts line
        end
    end
end

happy_bday = Song.new(["Happy birthday to you",
                   "I don't want to get sued",
                   "So I'll stop right there"])

bulls_on_parade = Song.new(["They rally around the family",
                        "With pockets full of shells"])

happy_bday.sing_me_a_song()

bulls_on_parade.sing_me_a_song()
		

Exercise 41: Learning to Speak Object-oriented

Just as a couple of exercises ago when trying to explain how Ruby scans, parses and executed code, the author has a rather strange way of looking at this stuff.

~/dev/ruby $ ruby ex41.rb

class Boot
	def initialize(cup)
	end
end

> class Boot has-a initialize that takes cup as a parameter.

ANSWER:  class Boot has-a initialize that takes cup parameters.

border = Cap.new()

> border gets a new Cap.

ANSWER:  Set border to an instance of class Cap.

cord.appliance = 'account'

> Set cord's appliance to the string "account".

ANSWER:  From cord get the appliance attribute and set it to 'account'.

chicken.doctor(bridge, beef)

> Call chicken's doctor passing bridge and beef as parameters.

ANSWER:  From chicken get the doctor function, and call it with parameters bridge, beef.

class Arch < Beetle
end

> Class Arch inherits class Beetle's attributes.

ANSWER:  Make a class named Arch that is-a Beetle.

class Cry
	def celery(drawer)
	end
end
^D
		

~/dev/ruby $ ruby ex41.rb english

From church get the cart attribute and set it to 'cord'.

> church.cart = 'cord'

ANSWER:  church.cart = 'cord'

class Bubble has-a function named crush that takes drug parameters.

> class Bubble def crush( drug ) end end

ANSWER:  class Bubble
	def crush(drug)
	end
end

Set channel to an instance of class Advertisement.

> channel = Advertisement.new()

ANSWER:  channel = Advertisement.new()

From door get the arithmetic function, and call it with parameters curtain, basketball.

> door.arithmetic( curtain, basketball )

ANSWER:  door.arithmetic(curtain, basketball)

Make a class named Cactus that is-a Aftermath.
^D
		
 

ex41.rb:

require 'open-uri'

WORD_URL = "http://learncodethehardway.org/words.txt"
WORDS = []

PHRASES = {
  "class ### < ###\nend" => "Make a class named ### that is-a ###.",
  "class ###\n\tdef initialize(@@@)\n\tend\nend"  => "class ### has-a initialize that takes @@@ parameters.",
  "class ###\n\tdef ***(@@@)\n\tend\nend" => "class ### has-a function named *** that takes @@@ parameters.",
  "*** = ###.new()"  => "Set *** to an instance of class ###.",
  "***.***(@@@)"  => "From *** get the *** function, and call it with parameters @@@.",
  "***.*** = '***'" => "From *** get the *** attribute and set it to '***'."
}

PHRASE_FIRST = ARGV[0] == "english"

open(WORD_URL) {|f|
  f.each_line {|word| WORDS.push(word.chomp)}
}

def craft_names(rand_words, snippet, pattern, caps=false)
  names = snippet.scan(pattern).map do
    word = rand_words.pop()
    caps ? word.capitalize : word
  end

  return names * 2
end

def craft_params(rand_words, snippet, pattern)
  names = (0...snippet.scan(pattern).length).map do
    param_count = rand(3) + 1
    params = (0...param_count).map {|x| rand_words.pop() }
    params.join(', ')
  end

  return names * 2
end

def convert(snippet, phrase)
  rand_words = WORDS.sort_by {rand}
  class_names = craft_names(rand_words, snippet, /###/, caps=true)
  other_names = craft_names(rand_words, snippet, /\*\*\*/)
  param_names = craft_params(rand_words, snippet, /@@@/)

  results = []

  for sentence in [snippet, phrase]
    # fake class names, also copies sentence
    result = sentence.gsub(/###/) {|x| class_names.pop }

    # fake other names
    result.gsub!(/\*\*\*/) {|x| other_names.pop }

    # fake parameter lists
    result.gsub!(/@@@/) {|x| param_names.pop }

    results.push(result)
  end

  return results
end

# keep going until they hit CTRL-D
loop do
  snippets = PHRASES.keys().sort_by {rand}

  for snippet in snippets
    phrase = PHRASES[snippet]
    question, answer = convert(snippet, phrase)

    if PHRASE_FIRST
      question, answer = answer, question
    end

    print question, "\n\n> "

    exit(0) unless STDIN.gets

    puts "\nANSWER:  %s\n\n" % answer
  end
end
		

Exercise 42: Is-a, Has-a, Objects and Classes

There is no coding exercise, this is all about the distinction between objects and classes, "is-a" and "has-a".

~/dev/ruby $ ruby ex42.rb

		
 

ex42.rb:

## Animal is-a object look at the extra credit
class Animal
end

## ??
class Dog < Animal

    def initialize(name)
        ## ??
        @name = name
    end
end

## ??
class Cat < Animal

    def initialize(name)
        ## ??
        @name = name
    end
end

## ??
class Person

    def initialize(name)
        ## ??
        @name = name

        ## Person has-a pet of some kind
        @pet = nil
    end

    attr_accessor :pet
end

## ??
class Employee < Person

    def initialize(name, salary)
        ## ?? hmm what is this strange magic?
        super(name)
        ## ??
        @salary = salary
    end

end

## ??
class Fish
end

## ??
class Salmon < Fish
end

## ??
class Halibut < Fish
end


## rover is-a Dog
rover = Dog.new("Rover")

## ??
satan = Cat.new("Satan")

## ??
mary = Person.new("Mary")

## ??
mary.pet = satan

## ??
frank = Employee.new("Frank", 120000)

## ??
frank.pet = rover

## ??
flipper = Fish.new()

## ??
crouse = Salmon.new()

## ??
harry = Halibut.new()
		

Exercise 43: Gothons from Planet Percal #25

This is a lot of code that's a game. I'm not going to put here. You're just supposed to trace through it to see how it works.

Exercise 44: Inheritance vs. Composition

Another volley in the "I don't get OOP series", the author clearly does and takes pains to explain the distinction in Ruby. What's to retain from all of this? The Ruby syntax, of course.

See the use of super().

~/dev/ruby $ ruby ex44-1.rb

PARENT implicit()
PARENT implicit()
		
 

ex44-1.rb:

class Parent
    def implicit()
        puts "PARENT implicit()"
    end
end

class Child < Parent
end

dad = Parent.new()
son = Child.new()

dad.implicit()
son.implicit()
		

~/dev/ruby $ ruby ex44-2.rb

PARENT override()
CHILD override()
		
 

ex44-2.rb:

class Parent
    def override()
        puts "PARENT override()"
    end
end

class Child < Parent
    def override()
        puts "CHILD override()"
    end
end

dad = Parent.new()
son = Child.new()

dad.override()
son.override()
		

~/dev/ruby $ ruby ex44-3.rb

PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()
		
 

ex44-3.rb:

class Parent
    def altered()
        puts "PARENT altered()"
    end
end

class Child < Parent
    def altered()
        puts "CHILD, BEFORE PARENT altered()"
        super()
        puts "CHILD, AFTER PARENT altered()"
    end

end

dad = Parent.new()
son = Child.new()

dad.altered()
son.altered()
		

~/dev/ruby $ ruby ex44-4.rb

PARENT implicit()
PARENT implicit()
PARENT override()
CHILD override()
PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered()
CHILD, AFTER PARENT altered()
		
 

ex44-4.rb:

class Parent
    def override()
        puts "PARENT override()"
    end

    def implicit()
        puts "PARENT implicit()"
    end

    def altered()
        puts "PARENT altered()"
    end
end

class Child < Parent
    def override()
        puts "CHILD override()"
    end

    def altered()
        puts "CHILD, BEFORE PARENT altered()"
        super()
        puts "CHILD, AFTER PARENT altered()"
    end
end

dad = Parent.new()
son = Child.new()

dad.implicit()
son.implicit()

dad.override()
son.override()

dad.altered()
son.altered()
		

~/dev/ruby $ ruby ex44-5.rb

OTHER implicit()
CHILD override()
CHILD, BEFORE OTHER altered()
OTHER altered()
CHILD, AFTER OTHER altered()
		
 

ex44-5.rb:

class Other
    def override()
        puts "OTHER override()"
    end

    def implicit()
        puts "OTHER implicit()"
    end

    def altered()
        puts "OTHER altered()"
    end
end

class Child
    def initialize()
        @other = Other.new()
    end

    def implicit()
        @other.implicit()
    end

    def override()
        puts "CHILD override()"
    end

    def altered()
        puts "CHILD, BEFORE OTHER altered()"
        @other.altered()
        puts "CHILD, AFTER OTHER altered()"
    end
end

son = Child.new()

son.implicit()
son.override()
son.altered()
		

~/dev/ruby $ ruby ex44-6.rb

OTHER implicit()
CHILD override()
CHILD, BEFORE OTHER altered()
OTHER altered()
CHILD, AFTER OTHER altered()
		
 

ex44-6.rb:

module Other
    def Other.override()
        puts "OTHER override()"
    end

    def Other.implicit()
        puts "OTHER implicit()"
    end

    def Other.altered()
        puts "OTHER altered()"
    end
end

class Child
    def implicit()
        Other.implicit()
    end

    def override()
        puts "CHILD override()"
    end

    def altered()
        puts "CHILD, BEFORE OTHER altered()"
        Other.altered()
        puts "CHILD, AFTER OTHER altered()"
    end
end

son = Child.new()

son.implicit()
son.override()
son.altered()
		

Exercise 45: You Make a Game

Coding assignment is to write a game. Good luck with that if you've nothing better to do. See Exercise 45: You Make a Game.

Exercise 46: A Project Skeleton

This exercise is interested in that it reveals the mind of one Ruby programmer's way of setting up Ruby projects. Not all of this is clear. For instance, the author uses the expression, project's root directory without defining it. Which of the following is that directory?

~/dev/ruby $ tree projects
projects
+-- skeleton
    + bin
    + lib
    | + NAME
    | | ` version.rb
    | ` NAME.rb
    + NAME.gemspec
    ` test
        ` test_NAME.rb

5 directories, 4 files

Note that NAME everywhere is to be replaced by the name of your project.

NAME.gemspec:
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "NAME/version"

Gem::Specification.new do |s|
  s.name        = "NAME"
  s.version     = NAME::VERSION
  s.authors     = ["Rob Sobers"]
  s.email       = ["[email protected]"]
  s.homepage    = ""
  s.summary     = %q{TODO: Write a gem summary}
  s.description = %q{TODO: Write a gem description}

  s.rubyforge_project = "NAME"

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]
end

In the previous file, we see the path "../lib" which encourages us not to see skeleton as the project root, but we'll have to see.

Here are the steps according to the author to use this skeleton.

  1. When ready to create and code a new project, copy this skeleton to where you wish to work and rename it appropriately.
  2. Rename skeleton, NAME, NAME.gemspec, NAME.rb and test_NAME.rb in the filesystem.
  3. Edit what used to be named NAME.gemspec and change occurrences of NAME as appropriate.

Assuming your new project is named, "accountmgr", you should see something like this afterward:

~/dev/ruby $ tree projects
projects
+-- accountmgr
    + bin
    + lib
    | + accountmgr
    | | ` version.rb
    | ` accountmgr.rb
    + accountmgr.gemspec
    ` test
        ` test_accountmgr.rb

5 directories, 4 files

...and...

# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "accountmgr/version"

Gem::Specification.new do |s|
  s.name        = "accountmgr"
  s.version     = accountmgr::VERSION
  s.authors     = ["Rob Sobers"]
  s.email       = ["[email protected]"]
  s.homepage    = ""
  s.summary     = %q{TODO: Write a gem summary}
  s.description = %q{TODO: Write a gem description}

  s.rubyforge_project = "accountmgr"

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]
end

A really good thing to do now is to read up on the gemspec file. For one thing, it appears that you should create a script in the bin subdirectory that's referenced from the gemspec file. This is not yet shown here. Here are some useful links to study.

Exercise 47: Automated Testing

Ah, now we're getting down to business: writing a test case...

See the structure under ruby/projects/ex47 to observe this exercise.

~/dev/ruby $ cd ruby/projects/ex47/test ; ruby test_ex47.rb

Run options:

# Running tests:

...

Finished tests in 0.000732s, 4100.5566 tests/s, 9567.9654 assertions/s.

3 tests, 7 assertions, 0 failures, 0 errors, 0 skips
		
 

ruby/projects/ex47/lib/ex47.rb:

class Room
  attr_accessor :name, :description, :paths

  def initialize(name, description)
    @name = name
    @description = description
    @paths = {}
  end

  def go(direction)
    @paths[direction]
  end

  def add_paths(paths)
    @paths.update(paths)
  end
end
		

ruby/projects/ex47/test/test_ex47.rb:

require 'test/unit'
require_relative '../lib/ex47'

class MyUnitTests < Test::Unit::TestCase

  def test_room()
    gold = Room.new("GoldRoom",
                    """This room has gold in it you can grab. There's a
                door to the north.""")
    assert_equal(gold.name, "GoldRoom")
    assert_equal(gold.paths, {})
  end

  def test_room_paths()
    center = Room.new("Center", "Test room in the center.")
    north = Room.new("North", "Test room in the north.")
    south = Room.new("South", "Test room in the south.")

    center.add_paths({:north => north, :south => south})
    assert_equal(center.go(:north), north)
    assert_equal(center.go(:south), south)
  end

  def test_map()
    start = Room.new("Start", "You can go west and down a hole.")
    west = Room.new("Trees", "There are trees here, you can go east.")
    down = Room.new("Dungeon", "It's dark down here, you can go up.")

    start.add_paths({:west => west, :down => down})
    west.add_paths({:east => start})
    down.add_paths({:up => start})

    assert_equal(start.go(:west), west)
    assert_equal(start.go(:west).go(:east), start)
    assert_equal(start.go(:down).go(:up), start)
  end
end
		

Best practices for Ruby testing...

  1. Put test files into the test subdirectory, one file per module or class tested.
  2. Keep test cases short. Test cases tend to be a bit messy.
  3. Keep test cases as clean as the natural messiness will permit. Create helper methods to reduce code duplication.
  4. I say, "Write tests first; then only write the production code based on the output from the tests". In other words, TEST FIRST, CODE LAST!

Exercise 48: Advanced User Input

Hoorah! We get to write a test again!

The trick of this exercise is to force you to create the Lexicon code from the given test code, a sort of brutal TDD exercise.

Exception handling

As (always!) compared to Java, ...

...in irb we can generate sample errors:

irb(main):005:0> Integer( 3 )
=> 3
irb(main):006:0> Integer( 3.2 )
=> 3
irb(main):007:0> Integer( cat )
NameError: undefined local variable or method `cat' for main:Object
	from (irb):7
	from /usr/bin/irb:12:in `<main>'
irb(main):008:0> Integer( "cat" )
ArgumentError: invalid value for Integer(): "cat"
	from (irb):8:in `Integer'
	from (irb):8
	from /usr/bin/irb:12:in `<main>'

These errors are caught via the rescue mechanism in Ruby:

def convert_number( number )
  begin
    Integer( number )
  rescue ArgumentError
    puts "ArgumentError--not a number"
    nil
  end
end

So, begin ... rescue ... end   is like   try { ... } catch { ... }.

~/dev/ruby/projects/lexicon/test $ ruby test_lexicon.rb

ruby test_lexicon.rb
Run options:

# Running tests:

north
north
south
east
.ASDFADFASDF
bear
IAS
princess
.bear
bear
princess
.1234
3
91234
.the
the
in
of
.go
go
kill
eat
.

Finished tests in 0.000892s, 6725.6506 tests/s, 13451.3012 assertions/s.

6 tests, 12 assertions, 0 failures, 0 errors, 0 skips
		
 

projects/lexicon/lib/lexicon.rb:

class Lexicon
  attr_accessor :lexicon, :dict

  def initialize()
    @dict = {
      :north => :direction,
      :south => :direction,
      :east  => :direction,
      :west  => :direction,
      :go    => :verb,
      :kill  => :verb,
      :eat   => :verb,
      :bear     => :noun,
      :princess => :noun,
      :the   => :stop,
      :in    => :stop,
      :of    => :stop,
    }
  end

  Pair = Struct.new(:qualifier, :value)

  def scan(list)
    pairs = []
    list.split.map do | value |
      puts value
      if value.match( /^[0-9]+$/ )
        pairs.push( Pair.new(:number, value.to_i) )
      elsif qualifier = @dict[value.intern]
        pairs.push( Pair.new(qualifier, value) )
      else
        pairs.push( Pair.new(:error, value.to_s) )
      end
    end
    pairs
  end
end
		

projects/lexicon/test/test_lexicon.rb:

require 'test/unit'
require_relative "../lib/lexicon/lexicon"

class LexiconTests < Test::Unit::TestCase

  Pair = Lexicon::Pair
  @@lexicon = Lexicon.new()

  def test_directions()
    assert_equal([Pair.new(:direction, 'north')], @@lexicon.scan("north"))
    result = @@lexicon.scan("north south east")
    assert_equal(result, [Pair.new(:direction, 'north'),
                 Pair.new(:direction, 'south'),
                 Pair.new(:direction, 'east')])
  end

  def test_verbs()
    assert_equal(@@lexicon.scan("go"), [Pair.new(:verb, 'go')])
    result = @@lexicon.scan("go kill eat")
    assert_equal(result, [Pair.new(:verb, 'go'),
                 Pair.new(:verb, 'kill'),
                 Pair.new(:verb, 'eat')])
  end

  def test_stops()
    assert_equal(@@lexicon.scan("the"), [Pair.new(:stop, 'the')])
    result = @@lexicon.scan("the in of")
    assert_equal(result, [Pair.new(:stop, 'the'),
                 Pair.new(:stop, 'in'),
                 Pair.new(:stop, 'of')])
  end

  def test_nouns()
    assert_equal(@@lexicon.scan("bear"), [Pair.new(:noun, 'bear')])
    result = @@lexicon.scan("bear princess")
    assert_equal(result, [Pair.new(:noun, 'bear'),
                 Pair.new(:noun, 'princess')])
  end

  def test_numbers()
    assert_equal(@@lexicon.scan("1234"), [Pair.new(:number, 1234)])
    result = @@lexicon.scan("3 91234")
    assert_equal(result, [Pair.new(:number, 3),
                 Pair.new(:number, 91234)])
  end

  def test_errors()
    assert_equal(@@lexicon.scan("ASDFADFASDF"), [Pair.new(:error, 'ASDFADFASDF')])
    result = @@lexicon.scan("bear IAS princess")
    assert_equal(result, [Pair.new(:noun, 'bear'),
                 Pair.new(:error, 'IAS'),
                 Pair.new(:noun, 'princess')])
  end
end
		

Exercise 49: Making Sentences

 

parsererror.rb:

		class ParserError < Exception

end

class Sentence

  def initialize(subject, verb, object)
    # remember we take Pair.new(:noun, "princess") structs and convert them
    @subject = subject.word
    @verb = verb.word
    @object = object.word
  end

end

module Parser

  def self.peek(word_list)
    begin
      word_list.first.token
    rescue
      nil
    end
  end

  def self.match(word_list, expecting)
    begin
      word = word_list.shift
      if word.token == expecting
        word
      else
        nil
      end
    rescue
      nil
    end
  end

  def self.skip(word_list, token)
    while peek(word_list) == token
      match(word_list, token)
    end
  end

  def self.parse_verb(word_list)
    skip(word_list, :stop)

    if peek(word_list) == :verb
      return match(word_list, :verb)
    else
      raise ParserError.new("Expected a verb next.")
    end
  end

  def self.parse_object(word_list)
    skip(word_list, :stop)
    next_word = peek(word_list)

    if next_word == :noun
      return match(word_list, :noun)
    end
    if next_word == :direction
      return match(word_list, :direction)
    else
      raise ParserError.new("Expected a noun or direction next.")
    end
  end

  def self.parse_subject(word_list, subj)
    verb = parse_verb(word_list)
    obj = parse_object(word_list)

    return Sentence.new(subj, verb, obj)
  end

  def self.parse_sentence(word_list)
    skip(word_list, :stop)

    start = peek(word_list)

    if start == :noun
      subj = match(word_list, :noun)
      return parse_subject(word_list, subj)
    elsif start == :verb
      # assume the subject is the player then
      return parse_subject(word_list, Pair.new(:noun, "player"))
    else
      raise ParserError.new("Must start with subject, object, or verb not: #{start}")
    end
  end
end
		

Exceptions...

...were begun last lesson. Here's how to generate them; compare with statements in the code above:

  raise ParserError.new( "Must start with subject, object or verb." )

Modules

The code above is an illustration of using an existing module named Parser, which is written here as the bottom majority of this code. It's a way to package up methods so that they don't conflict with anything else in Ruby or your Ruby code. In essence, the Ruby module wraps all the methods with a name. You consume module methods merely by stating the module name, adding the dot operator and then the name of the method you want to use.

  Parser.parse_verb( word_list )

We're done for...

At this point, I've bogged down too much and am out of time. My only purpose was to learn Ruby syntax and constructs. I found learning Ruby practices like the organization of a Ruby project fascinating, but I need to get back to Chef and using Ruby for that. The remainder of these exercises is left for another time.


Exercise 50: Your First Website

Exercise 51: Getting Input from a Browser

Exercise 52: The Start of Your Web Game




Exercise 0: The Setup
Exercise 1: A Good First Program
Exercise 2: Comments and Pound Characters
Exercise 3: Numbers and Math
Exercise 4: Variables and Names
Exercise 5: More Variables and Printing
Exercise 6: Strings and Text
Exercise 7; More Printing
Exercise 8: Printing, Printing
Exercise 9: Printing, Printing, Printing
Exercise 10: What Was That? (escape sequences)
Exercise 11: Asking Questions
Exercise 12: Libraries
Exercise 13: Parameters, Unpacking, Variables
Exercise 14: Prompting and Passing
Exercise 15: Reading Files
Exercise 16: Reading and Writing Files
Exercise 17: More Files
Exercise 18: Names, Variables, Code, Functions
Exercise 19: Functions and Variables
Exercise 20: Functions and Files
Exercise 21: Functions Can Return Something
Exercise 22: What Do You Know So Far?
Exercise 23: Read Some Code
Exercise 24: More Practice (escape sequences)
Exercise 25: Even More Practice (functions and variables)
Exercise 26: Congratulations, Take a Test!
Exercise 27: Memorizing Logic
Exercise 28: Boolean Practice
Exercise 29: What If
Exercise 30: Else and If
Exercise 31: Making Decisions
Exercise 32: Loops and Arrays
Exercise 33: While Loops
Exercise 34: Accessing Elements of Arrays
Exercise 35: Branches and Functions
Exercise 36: Designing and Debugging
Exercise 37: Symbol Review
Exercise 38: Doing Things to Lists
Exercise 39: Hashes, Oh Lovely Hashes
Exercise 40: Modules, Classes and Objects
Exercise 41: Learning to Speak Object-oriented
Exercise 42: Is-a, Has-a, Objects and Classes
Exercise 43: Gothons from Planet Percal #25
Exercise 44: Inheritance vs. Composition
Exercise 45: You Make a Game
Exercise 46: A Project Skeleton
Exercise 47: Automated Testing
Exercise 48: Advanced User Input
Exercise 49: Making Sentences
Exercise 50: Your First Website
Exercise 51: Getting Input from a Browser
Exercise 52: The Start of Your Web Game
Appendix