Ruby tutorials
What is Ruby?
Ruby is an open-source object-oriented scripting language invented in the mid-90s by Yukihiro Matsumoto.
Unlike languages such as C and C++, a scripting language doesn’t talk directly to hardware.
Programs are generally procedural in nature, meaning they are read from top to bottom.
Object-oriented languages, on the other hand, break out pieces of code into objects that can be created and used as needed.
You can reuse these objects in other parts of the program, or even other applications.
What can Ruby be used for?
The Ruby programming language is a highly portable general-purpose language that serves many purposes.
Ruby is great for building desktop applications, static websites, data processing services, and even automation tools.
It’s used for web servers, DevOps, and web scraping and crawling.
And when you add in the functionality of the Rails application framework, you can do even more, especially database-driven web applications.
Ruby on Rails
Ruby stands alone as a high-level programming language.
But you really can't talk about Ruby without mentioning Rails.
Ruby on Rails is the application framework that thrust Ruby into the spotlight, boosted its popularity, and made it a great language for the cloud.
The Ruby on Rails framework consists of pre-written Ruby code for things like communication, file handling, database connections, and more.
It takes care of the tedious items, so you can focus on solving problems.
One of the key concepts of Rails is DRY — Don’t Repeat Yourself — which is key to the framework’s efficiency.
There are over a million websites written in Ruby on Rails.
Ruby vs. Python
Ruby and Python have a lot in common.
Both Ruby and Python are high-level server-side scripting languages with clear and easy-to-read syntax, but there are some important technical differences.
Differences between Ruby vs Python
Some of the differences between Ruby vs Python include:
Python supports multiple IDEs, whereas Ruby supports only EclipseIDE.
With Python you’re limited to the Django framework; with Ruby, you’re limited to Rails.
Ruby uses a powerful blocks feature, but Python offers more libraries.
Ruby is a true object-oriented language, but Python has more traction among data scientists.
And on it goes, tit for tat.
Then there are the more subtle differences, too.
Some find Python easier to learn initially but more stifling in the long run.
In many ways, it boils down to a basic philosophical difference between the two: In Ruby, there are many ways of doing things, many solutions to one problem.
In Python, there’s a best way of doing things, and that’s the way you should do it.
Why should I learn Ruby?
The Ruby programming language is designed for programmer productivity and fun.
Developers like using Ruby because it’s high level and has a simple syntax.
You have less code to write and can focus on finding a solution to your problem.
While many low-level languages require lines and lines of code for the smallest thing, with Ruby, you can write your first cloud application in just a few hours.
Ruby is "A Programmer's Best Friend".
Ruby has features that are similar to those of Smalltalk, Perl, and Python.
Perl, Python, and Smalltalk are scripting languages. Smalltalk is a true object-oriented language.
Ruby, like Smalltalk, is a perfect object-oriented language.
Using Ruby syntax is much easier than using Smalltalk syntax.
Features of Ruby
Ruby is an open-source and is freely available on the Web, but it is subject to a license.
Ruby is a general-purpose, interpreted programming language.
Ruby is a true object-oriented programming language.
Ruby is a server-side scripting language similar to Python and PERL.
Ruby can be used to write Common Gateway Interface (CGI) scripts.
Ruby can be embedded into Hypertext Markup Language (HTML).
Ruby has a clean and easy syntax that allows a new developer to learn very quickly and easily.
Ruby has similar syntax to that of many programming languages such as C++ and Perl.
Ruby is very much scalable and big programs written in Ruby are easily maintainable.
Ruby can be used for developing Internet and intranet applications.
Ruby can be installed in Windows and POSIX environments.
Ruby support many GUI tools such as Tcl/Tk, GTK, and OpenGL.
Ruby can easily be connected to DB2, MySQL, Oracle, and Sybase.
Ruby has a rich set of built-in functions, which can be used directly into Ruby scripts.
Sum Of Two Numbers
In this example we want to find out if given an array of unique numbers, there is a combination of two numbers which adds up to a target number.
Code:
def sum_eq_n?(arr, n)
return true if arr.empty? && n == 0
arr.product(arr).reject { |a,b| a == b }.any? { |a,b| a + b == n }
end
This is interesting because I'm using the product
method here.
When you use this method is like having a loop inside a loop that combines all values in array A with all values in array B.
Counting, Mapping & Finding
Let's say that you want to find the missing number in an arithmetic sequence, like (2,4,6,10)
.
We can use a strategy where we calculate the difference between the numbers.
[2, 2, 4]
Our goal here is to find out what the sequence is.
Is it increasing or decreasing?
By how much?
This code reveals the sequence:
differences = [2, 2, 4]
differences.max_by { |n| differences.count(n) }
# 2
# This is the increase between numbers in the sequence
Once we know the sequence we can compare all the numbers to find the missing one.
Here's the code:
def find_missing(sequence)
consecutive = sequence.each_cons(2)
differences = consecutive.map { |a,b| b - a }
sequence = differences.max_by { |n| differences.count(n) }
missing_between = consecutive.find { |a,b| (b - a) != sequence }
missing_between.first + sequence
end
find_missing([2,4,6,10])
# 8
Count how many Ruby methods we're using here to do the hard work for us 🙂
Regular Expression Example
If you are working with strings & you want to find patterns then regular expressions are your friend.
They can be a bit tricky to get right, but practice makes mastery!
Now:
Let's say that we want to find out if a given string follows a pattern of VOWEL to NON-VOWEL characters.
Like this:
"ateciyu"
Then we can use a regular expression, along with the match?
method to figure this out.
Here's the code example:
def alternating_characters?(s)
type = [/[aeiou]/, /[^aeiou]/].cycle
if s.start_with?(/[^aeiou]/)
type.next
end
s.chars.all? { |ch| ch.match?(type.next) }
end
alternating_characters?("ateciyu")
# true
Notice a few things:
We use the cycle
method so we can keep switching between the VOWEL regex & the NON-VOWEL regex.
We convert the string into an array of characters with chars
so that we can use the all?
method.
Recursion & Stack Example
Recursion is when a method calls itself multiple times as a way to make progress towards a solution.
Many interesting problems can be solved with recursion.
But because recursion has its limits, you can use a stack data structure instead.
Now:
Let's look at an example where we want to find out the “Power Set” of a given array. The Power Set is a set of all the subsets that can be created from the array.
Here's an example with recursion:
def get_numbers(list, index = 0, taken = [])
return [taken] if index == list.size
get_numbers(list, index+1, taken) +
get_numbers(list, index+1, taken + [list[index]])
end
get_numbers([1,2,3])
Here's the same problem solved using a stack:
def get_numbers_stack(list)
stack = [[0, []]]
output = []
until stack.empty?
index, taken = stack.pop
next output << taken if index == list.size
stack.unshift [index + 1, taken]
stack.unshift [index + 1, taken + [list[index]]]
end
output
end
The idea here is that on each pass of the algorithm we are either taking a number or not taking a number.
We branch out & try both outcomes so we can produce all the possible combinations.
Imagine a tree where each leaf is one of the solutions.
A few things to notice:
The recursion solution is shorter
The actual "making progress" part of the algorithm (index + 1) is almost the same
The stack we're using is just an array because there isn't a Stack
class in Ruby
Method Chaining Example
This is my favorite example because it shows how powerful Ruby is.
Combining methods allows you to take the output produced by one method & pass it into another.
Just like a factory production line!
You start with some raw materials (input), then through the process of calling these methods, you slowly transform the raw materials into the desired result.
Here's an example:
def longest_repetition(string)
max = string
.chars
.chunk(&:itself)
.map(&:last)
.max_by(&:size)
max ? [max[0], max.size] : ["", 0]
end
longest_repetition("aaabb")
# ["a", 3]
Given a string, this code will find the longest repeated character.
Note:
How this code is formatted to maximize readability
Use of the Symbol#to_proc
pattern (&:size
)
Btw, don't confuse this with the "Law of Demeter".
That "law" is about reaching out into the internals of another object.
Here we are only transforming objects.
With Index Example
Would you like to have the current index while iterating over a collection of items?
You can use the with_index
method.
Here's an example:
def reverse_alternate(string)
string.gsub(/[^\s]+/).with_index { |w, idx| idx.even? ? w : w.reverse }
end
reverse_alternate("Apples Are Good")
# "Apples erA Good"
Notice:
We combine with_index
& even?
to find if we have to reverse the current word
Gsub without a block returns an Enumerator
object, which allows you to chain it with other methods
Each With Object Example
Another interesting method is each_with_object
, and its friend with_object
.
You can use these two methods when you need an object to hold the results.
Here's an example:
def clean_string(str)
str
.chars
.each_with_object([]) { |ch, obj| ch == "#" ? obj.pop : obj << ch }
.join
end
clean_string("aaa#b")
In this example, we want to delete the last character when we find a #
symbol.
Notice:
each_with_object
takes an argument, which is the object we want to start with. This argument becomes the 2nd block parameter.
We are converting the string into an array of characters (char
) & then back to a string when we are done (join
).
We are using a ternary operator to decide what to do, this makes the code shorter.
Ruby Hello, World!
print "Hello, World!\n"
The Ruby Hello, World! program is a one-liner.
C-like (really perl-like) string constant
No semicolon needed (though it wouldn't break anything).
# Variables and expressions.
a = 10
b = 3 * a + 2
printf("%d %d\n", a, b);
# Type is dynamic.
b = "A string"
c = 'Another String'
print b + " and " + c + "\n"
The variables shown in this example are "local variables".
(We'll explore other possibilities later.)
Names are mostly conventional: letters, underscore, and digit, not starting with a digit.
Local variables must start with a lower-case letter.
Variable types are dynamic: Assignment transfers both and type, not just value.
Variables are not declared with a type, and assignment does not need to perform conversion.
Obviously, the +
concatenates strings, as in Java and C++.
The printf
function is lifted from plain C.
Objects
# Operators are really method invocations.
a = 10
b = 3.*(a).+(2)
Kernel::printf("%d %d\n", a, b);
# Type is still dynamic.
b = String.new("A string")
c = 'Another String'
Kernel.print(b.+(" and ")::+(c).+("\n"))
Here is a more honest version of the last example.
Ruby
is a fairly pure object-oriented language, which borrows a good bit from Smalltalk.
Pretty much everything is an object.
Ruby has a bit more complex syntax than Smalltalk, some of it designed to make much of Ruby look more conventional.
But it's
invoking methods on objects just the same.
For instance, ordinary integers are objects of the class
Fixnum
.
The numeric operations are methods.
Ruby lets you write 3 * a
, but what you're really doing is invoking the *
method on the object 3
, and you can write it that way
3.*(a)
.
The name Kernel
refers to a built-in module.
A module is similar to a static class in Java.
The builtin-in functions print
and
printf
are part of Kernel
.
As far as I can tell, the ::
and .
operators are equivalent.
Of course, string constants are objects of class String
, and this is made explicit in the statement b = String.new("A String");
.
Notice that new
is a method of class String
, rather than being an operator as in Java and C++.
We'll usually use the more conventional syntax, but the heavy use of objects provides gread flexibilty.
We'll flex later.
Also note that there is no sense of Java's wrapper classes,
like Integer
.
When you write an integer constant, it's an object of
Ruby's Integer
class.
There is no int
.
Strings
# Double-quoted strings can substitute variables.
a = 17
print "a = #{a}\n";
print 'a = #{a}\n';
print "\n";
# If you're verbose, you can create a multi-line string like this.
b = <<ENDER
This is a longer string,
perhaps some instructions or agreement goes here.
By the way,
a = #{a}.
ENDER
print "\n[[[" + b + "]]]\n";
print "\nActually, any string can span lines.
The line\nbreaks just become part of the string.
"
print %Q=\nThe highly intuitive "%Q" prefix allows alternative delimiters.\n=
print %Q[Bracket symbols match their mates, not themselves.\n]
Ruby has many ways of making strings, which are generally variations of the many ways perl has of making strings.
Double quotes allow values to be interpolated into the string,
while single quotes do not.
Most escapes are treated literally in single quotes, including the fact that \n
is treated as two characters, a backward slash followed by an n.
Ruby strings may extend any number of lines.
This can be useful,
but make sure you don't leave out any closing quotes.
The <<
notation follows perl and other languages as a way to multi-line strings.
Those don't generally permit the other varieties to specify multi-line strings, so it's actually kind of redundant in Ruby.
The %
can be used to create strings using a different
delimiter.
%Q
x starts a double-quote style string which ends with the next x, rather than the usual double quote character.
%q
y starts a single-quote string.
String Operations
s = "Hi there.
How are you?"
print s.length, " [" + s + "]\n"
# Selecting a character in a string gives an integer ascii code.
print s[4], "\n"
printf("%c\n", s[4])
# The [n,l] substring gives the starting position and length.
The [n..m]
# form gives a range of positions, inclusive.
print "[" + s[4,4] + "] [" + s[6..15] + "]\n"
print "Wow " * 3, "\n"
print s.index("there"), " ", s.index("How"), " ", s.index("bogus"), "\n"
print s.reverse, "\n"
Arrays
a = [ 45, 3, 19, 8 ]
b = [ 'sam', 'max', 56, 98.9, 3, 10, 'jill' ]
print (a + b).join(' '), "\n"
print a[2], " ", b[4], " ", b[-2], "\n"
print a.sort.join(' '), "\n"
a << 57 << 9 << 'phil'
print "A: ", a.join(' '), "\n"
b << 'alex' << 48 << 220
print "B: ", b.join(' '), "\n"
print "pop: ", b.pop, "\n"
print "shift: ", b.shift, "\n"
print "C: ", b.join(' '), "\n"
b.delete_at(2)
b.delete('alex')
print "D: ", b.join(' '), "\n"
Ruby arrays have operations similar to strings.
As the quote notation creates a String object, the bracket notation creates an Array object.
Hashes
z = { 'mike' => 75, 'bill' => 18, 'alice' => 32 }
z['joe'] = 44
print z['bill'], " ", z['joe'], " ", z["smith"], "\n"
print z.has_key?('mike'), " ", z.has_key?("jones"), "\n"
Ruby hashes are similar to maps or dictionaries in other languages.
They are
essentially arrays whose subscripts are not limited to integer values.
You can create them with the curly-bracked list notation shown, but you can also assign to a subscript expression to add members.
It's not at all unreasonable to create a hash with empty brackets, then add members using subscripting.
Method names may end in ?
, hence the name of the
has_key?
method.
This suggests a the method is a test which returns a boolean value.
Such methods are sometimes known as predicates.
Line Breaking
a = 10
b = a +
10
c = [ 5, 4,
10 ]
d = [ a ] \
+ c print "#{a} #{b} [", c.join(" "), "] [", d.join(" "), "]\n";
Ruby generally uses line breaks instead of semicolons to separate statements.
Lines can be continued by using a \ at the end, but this is rarely needed.
If the line ends with pretty much anything that suggests there should be more, Ruby will continue on to the next line.
This includes such things as ending with an operator, or inside parentheses or something else which needs to be closed.
Basic I/O I
# Get the parts of speech print "Please enter a past-tense verb: "
verb = gets.chomp print "Please enter a noun: "
noun = gets.chomp print "Please enter a proper noun: "
prop_noun = gets.chomp print "Please enter a an adverb: "
adv = gets.chomp
# Make the sentence.
print "#{prop_noun} got a #{noun} and\n#{verb} #{adv} around the block.\n"
See:Ruby Manual
The built-in gets
function simply reads a line and returns it as a string.
The chomp
method from
trims the line terminator.
The prompts are generated with ordinary prints lacking a final newline.
Basic I/O II
print "Triangle height: "
h = gets.to_f;
print "Triangle width: "
w = gets.to_f;
area = 0.5*h*w print "Triangle height ", h, " width ", w, " has area ", area, "\n"
See:Ruby Manual
Similar to the last one, the to_f
method of
converts the input string to a floating-point number.
File I/O
# Get the parts of speech.
print "Please enter a past-tense verb: "
verb = gets.chomp print "Please enter a noun: "
noun = gets.chomp print "Please enter a proper noun: "
prop_noun = gets.chomp print "Please enter a an adverb: "
adv = gets.chomp
# See where to put it.
print "Please enter a file name: "
fn = gets.chomp handle = open(fn,"w")
# Go.
printf(handle, "%s got a %s and\n%s %s around the block.\n",
prop_noun, noun, verb, adv)
handle.close
See:Ruby Manual
This version opens a file for writing, and writes its
output to a file selected by the user.
The second argument to open
is the same as the second argument to a plain C fopen
, except it will default to reading.
The object returned from open
is
.
Parallel Asst.
# Assign three values.
a, b, c = 8, 10, 15
print "A: a = ", a, ", b = ", b, ", c = ", c, "\n"
# Compute three values, then assign three values.
a, b, c = 40, a + 11, a + b + c print "B: a = ", a, ", b = ", b, ", c = ", c, "\n"
# Swap.
a, b = b, a print "C: a = ", a, ", b = ", b, ", c = ", c, "\n"
# Extras on left get nil.
a, b, c = 2, 3
print "D: a = ", a, ", b = ", b, ", c = ", c, "\n"
# Extras on right get left behind a, b, c = 11, 12, 13, 14, 15
print "E: a = ", a, ", b = ", b, ", c = ", c, "\n"
# The right can be an array, in which case the members are assigned to
# individual variables.
fred = [ 4, 5, 6, 7]
a, b, c = fred print "F: a = ", a, ", b = ", b, ", c = ", c, "\n"
A parallel assignment sets several variables at once.
All right sides are computed before any variable is assigned, so all right side values are computed with the old variable values.
This is very convenient for swapping two values without using a temporary.
When the right side is an array, the members of the array are broken out and assigned to individual variables.
Conditional I
# Pick a random number.
rno = rand(100) + 1
print "Your magic number is ", rno, "\n"
# Perform all sort of totally uselss test on it and report the results.
if rno % 2 == 1 then
print "Ooooh, that's an odd number.\n"
else
print "That's an even number.\n"
if rno > 2 then
print "It's not prime, BTW.\n"
end end
if rno > 50
print "That's more than half as big as it could be!\n"
elsif rno == 42
print "That's the ultimate magic number!!!!\n"
elsif rno < 10
print "That's pretty small, actually.\n"
else
print "What a boring number.\n"
end
if rno == 100 then print "Gosh, you've maxxed out!\n" end
This is the conventional use of the if
in perl.
Notice that there is no need for curly braces ({
and }
).
The body of the if
ends with the appropriate keyword,
end
, else
or elsif
.
The then
word is generally optional, though you need it if you want to put start the body on the same line as the if
,
the way the last statement does.
As in most languages (excluding Python), indenting is not required,
and is ignored by the interpreter.
Of course, it is wise to indent in a way which reflects the structure of the code.
Conditional II
# Let the user guess.
print "Enter heads or tails? "
hort = gets.chomp unless hort == 'heads' || hort == 'tails'
print "I _said_ heads or tails.
Can't you read?\n"
exit(1)
end
# Now toss the coin.
toss = if rand(2) == 1 then
"heads"
else
"tails"
end
# Report.
print "Toss was ", toss, ".\n"
print "You Win!\n" if hort == toss
Some more amazing if
lore:
The unless
keyword is like if
, but uses the opposite sense of the test: The code is run if the test is false.
As with many things in Ruby, if
may look like a statement,
but it is actually an expression, with a return value.
The if
has a postfix form where the condition comes last.
This works with unless
as well.
While Loop
# Some counting with a while.
a = 0
while a < 15
print a, " "
if a == 10 then
print "made it to ten!!"
end
a = a + 1
end print "\n"
# Here's a way to empty an array.
joe = [ 'eggs.', 'some', 'break', 'to', 'Have' ]
print(joe.pop, " ") while joe.size > 0
print "\n"
The while
loop is conventional, but it also has a postfix form.
For Loop
# Simple for loop using a range.
for i in (1..4)
print i," "
end print "\n"
for i in (1...4)
print i," "
end print "\n"
# Running through a list (which is what they do).
items = [ 'Mark', 12, 'goobers', 18.45 ]
for it in items
print it, " "
end print "\n"
# Go through the legal subscript values of an array.
for i in (0...items.length)
print items[0..i].join(" "), "\n"
end
The Ruby for
loop is similar to the Python for
or the perl foreach
.
Its basic operation is iterate over the contents of some
collection.
This may be an actual array or a range expression.
A range expression is two numbers, surrounded by parens, and separated by either two dots or three.
The three-dot form omits the last number from the range, while the two-dot version includes it.
The three-dots version can be quite useful for creating a range of legal subscripts since the array size is not a legal subscript.
Case Expression
for i in (1..10)
rno = rand(100) + 1
msg = case rno
when 42: "The ultimate result."
when 1..10: "Way too small."
when 11..15,19,27: "Sorry, too small"
when 80..99: "Way to large"
when 100:
print "TOPS\n"
"Really way too large"
else "Just wrong"
end
print "Result: ", rno, ": ", msg, "\n"
end
The ruby case
statement is similar to the C/C++/Java switch
, but more directly related to the similar (and superior)
structures from Pascal and Ada.
First, it assumes that each case ends where the next one starts, without needing a break
to terminate a case.
Secondly, each case can be
expressed rather generally, with a single value, a range of value, or a list containing some of each.
For this example, I'm using the fact the control statements have a return value.
That's not special to case
s, and their value can be used or not, just as if
s.
Iterators
# Here's a different way to add up an array.
fred = [ 4, 19, 3, 7, 32 ]
sum = 0
fred.each { |i| sum += i }
print "Sum of [", fred.join(" "), "] is #{sum}\n"
# Or create a secret message.
key = { 'A' => 'U', 'B' => 'Q', 'C' => 'A', 'D' => 'F', 'E' => 'D', 'F' => 'K',
'G' => 'P', 'H' => 'W', 'I' => 'N', 'J' => 'L', 'K' => 'J', 'L' => 'M',
'M' => 'S', 'N' => 'V', 'O' => 'Y', 'P' => 'O', 'Q' => 'Z', 'R' => 'T',
'S' => 'E', 'T' => 'I', 'U' => 'X', 'V' => 'B', 'W' => 'G', 'X' => 'H',
'Y' => 'R', 'Z' => 'C' }
print "\nThe encoded message is: "
"The secret message".each_byte do | b |
b = b.chr.upcase
if key.has_key?(b) then
print key[b]
else
print b
end end print "\n"
# But give us the info to read it anyway.
print "The key is: "
ct = 8
key.each { | k, v |
if ct == 8 then
print "\n "
ct = 0
else
print ", "
end
ct = ct + 1
print "#{v} => #{k}"
}
print "\n\n"
# Some interesting things from Integer.
3.times { print "Hi! " }
print "\n"
print "Count: "
3.upto(7) { |n| print n, " " }
print "\n"
See:Ruby Manual
Ruby iterators are methods which take and run a block of code.
The block can be delimited by curly braces or by the keywords
do
and end
.
The brackets have higher precedence, and variables declared in them are destroyed when the bracked code exits.
The parameter names for the block are listed between |
symbols at the start of the block.
This syntax is borrowed from Smalltalk.
Note: Ruby uses the term "iterator" rather differently than C++.
Methods I
# Square the number def sqr(x)
return x*x end
# See how it works.
(rand(4) + 2).times {
a = rand(300)
print a,"^2 = ", sqr(a), "\n"
}
print "\n"
# Don't need a parm.
def boom
print "Boom!\n"
end boom boom
# Default parms print "\n"
def line(cnt, ender = "+", fill = "-")
print ender, fill * cnt, ender, "\n"
end line(8)
line(5,'*')
line(11,'+','=')
# Do they change?
def incr(n)
n = n + 1
end a = 5
incr(a)
print a,"\n"
Functions (and later methods) are defined with def
.
Parameters are given, without types of course.
Default values may be provided.
Note that when we send an integer, the method does not change the value sent.
Methods II
# Place the array in a random order.
Floyd's alg.
def shuffle(arr)
for n in 0...arr.size
targ = n + rand(arr.size - n)
arr[n], arr[targ] = arr[targ], arr[n] if n != targ
end end
# Make strange declarations.
def pairs(a, b)
a << 'Insane'
shuffle(b)
b.each { |x| shuffle(a); a.each { |y| print y, " ", x, ".\n" } }
end first = ['Strange', 'Fresh', 'Alarming']
pairs(first, ['lemonade', 'procedure', 'sounds', 'throughway'])
print "\n", first.join(" "), "\n"
Here's another use of methods.
Notice that when we send an array as a parameter,
changes in the function do update the value to the caller.
One incidental thing we have not seen before is parallel assignment in the shuffle
function which exchanges two values.
Methods III
# Add the strings before and after around each parm and print def surround(before, after, *items)
items.each { |x| print before, x, after, "\n" }
end
surround('[', ']', 'this', 'that', 'the other')
print "\n"
surround('<', '>', 'Snakes', 'Turtles', 'Snails', 'Salamanders', 'Slugs',
'Newts')
print "\n"
def boffo(a, b, c, d)
print "a = #{a} b = #{b}, c = #{c}, d = #{d}\n"
end
# Use * to adapt between arrays and arguments a1 = ['snack', 'fast', 'junk', 'pizza']
a2 = [4, 9]
boffo(*a1)
boffo(17, 3, *a2)
The *
is used to adapt between arrays and parameter or argument lists.
In a call, a *
before an array treats the array members as separate arguments.
A *
before the last parameter causes it to receive all remaining arguments as an array.
Exceptions I
# Count and report the number of lines and characters in a file.
print "File name: "
fn = gets.chomp begin
f = open(fn)
nlines = 0
length = 0
f.each { |line| nlines += 1; length += line.length }
rescue
print "File read failed: " + $! + "\n"
else
print fn, ": ", nlines, " lines, ", length, " characters.\n"
end
Ruby uses exceptions.
Instead of
try
, the block is called begin
, and instead of
catch
there is rescue
.
The else
is executed when no exception occurs.
There is also an
ensure
block which is always run last, exception or no.
Regular Expressions I
# Get a chomped string, or nil at EOF.
def getstr
print "Please enter a test string: "
str = gets
return str unless str
return str.chomp end
# Test strings while str = getstr
print "You entered: ", str, "\n"
# Run some random tests and print a descriptive message for ones which
# match.
num = 0
if str =~ /^\s*$/ then
print " > Your string is all blanks.\n"
next
end
if str =~ /Mommy/ then
print " > Contains Mommy\n"
num += 1
end
if str =~ /Mommy.*Daddy/ then
print " > Contains Mommy, then Daddy\n"
num += 1
end
if str !~ /CAT/ then
print " > Does not contain CAT.\n"
num += 1
end
if str !~ /[Cc][Aa][Tt]/ then
print " > Does not contain cat (any capitalization).\n"
num += 1
end
if str =~ /^AA/ then
print " > Starts with AA\n"
num += 1
end
if str =~ /(ing|ed)$/ then
print " > Ends in ing or ed\n"
num += 1
end
if str =~ /^\d+$/ then
print " > Is an unsigned integer\n"
num += 1
end
if str =~ /^(\+|\-)\d+$/ then
print " > Is a signed integer\n"
num += 1
end
if str !~ /[AEIOUaeiou]/ then
print " > Contains no vowels.\n"
num += 1
end
if str =~ /@[^A-Z]*$/ then
print " > Has an at sign with no upper case letters following it.\n"
num += 1
end
if str =~ /^[^%]*%[^%]*%[^%]*%[^%]*$/ then
print " > Contains exactly 3 percent signs.\n"
num += 1
end
if str =~ %r=^(http|ftp)://([a-zA-Z-]+(\.[a-zA-Z-]+)+)(/|$)= then
proto = $1
host = $2
print " > Looks like a lot of common URLs with protocol #{proto} ",
"and host #{host}.\n"
num += 1
end
# What happened?
if num == 0 then
print "=== That string is remarkably boring. ===\n"
else
print "=== Found ", num,
" interesting thing" + (if num > 1 then "s" else "" end),
" about that string. ===\n"
end end
Regular expressions are patterns which can be matched against strings.
The usual syntax is to surround them with slashes, which create a
Regexp
object, much as putting quoted characters create a
String
object.
As the %Q
for strings, the
%r
notation allows an alternate delimiter on regular expressions.
In this program, use the =~
operator to ask if a string matches the pattern, and !~
to ask if it doesn't.
Parentheses used in regular expressions group operations just as the do in ordinary expressions.
In addition,
the characters matched by each parenthetical group are assigned to a special variable, $
n, where the left paren was the nth from the left.
Regular expressions are a large topic, and we'll have some class discussion about them.
Regular Expressions II
# Using re's to break up a line.
print "Please enter a line: "
line = gets.chomp
res = [ ]
while res != ''
# String leading blanks.
line.sub!(/^\s*/, '')
break if line == ''
# See what the leading is for next action.
if line[0].chr == '"' then
# Quoted.
line.sub!(/^"([^"]*)"/, '')
res.push($1)
elsif line.sub!(/^(\d+):(\S+)/, '')
# Repeated with n:
$1.to_i.times { res.push($2) }
else
# Just a word.
line.sub!(/^(\S+)/, '')
res.push($1)
end
end
res.each { |x| print " [", x, "]\n" }
The sub
and sub!
methods of String
match their first argument (a regular expression), and replace the portion matched with their second (a string).
This program breaks up the input line into words, with two special notations.
Double-quoted sections are obeyed, which can contain spaces, and a word can be repeated by placing a count and a colon before it, like 5:fred
instead of typing fred
five times.
More List Ops
fred = [ 4, 9, 18, 3, 87, 9, 12 ]
alex = [ 'Susan', 'Joe', 'Alex', 'Alice', 'Sam' ]
# Compute a new array with each member of fred doubled.
fred = fred.map { |x| 2 * x }
print fred.join(" "), "\n"
# Create a new alex adding " went away" to each member.
Then join and
# print the result.
print (alex.map { |z| z + " went away" }).join(" "), "\n"
# Print the members of fred which are more than five and less than 20.
print (fred.select { |z| z > 5 & z < 20 }).join(" "), "\n"
# Print the lengths of the members of alex that start with A or end with e.
print ((alex.select { |n| n =~ /^A/ || n =~ /e$/ }).map { |z| z.length }).
join(" "), "\n"
# Update alex by surround each of its members with [ ]
alex.map! { |a| "[" + a + "]" }
print alex.join(" "), "\n"
The built-in Array
class contains a number of iterators which work on the entire list and return a list result.
Two important ones are map
, which runs the code block on each member of the list, and returns the list of results from those operations.
The other is select
, which also runs a code block on each member of the list.
This block should return a boolean,
and a new list is build of only those members for which the code block produces true.
The code block can be thought of as a filter which decides, for each member, if it should be retained.
The plain forms, map
and select
return a new array.
The emphatic forms, map!
and select!
update the list.
Polynomial Evaluator
#
# This program evaluates polynomials.
It first asks for the coefficients
# of a polynomial, which must be entered on one line, highest-order first.
# It then requests values of x and will compute the value of the poly for
# each x.
It will repeatly ask for x values, unless you the user enters
# a blank line.
It that case, it will ask for another polynomial.
If the
# user types quit for either input, the program immediately exits.
#
#
# Function to evaluate a polynomial at x.
The polynomial is given
# as a list of coefficients, from the greatest to the least.
def polyval(x, coef)
sum = 0
coef = coef.clone # Don't want to destroy the original
while true
sum += coef.shift # Add and remove the next coef
break if coef.empty? # If no more, done entirely.
sum *= x # This happens the right number of times.
end
return sum end
#
# Function to read a line containing a list of integers and return
# them as an array of integers.
If the string conversion fails, it
# throws TypeError.
If the input line is the word 'quit', then it
# converts it to an end-of-file exception def readints(prompt)
# Read a line
print prompt
line = readline.chomp
raise EOFError.new if line == 'quit' # You can also use a real EOF.
# Go through each item on the line, converting each one and adding it
# to retval.
retval = [ ]
for str in line.split(/\s+/)
if str =~ /^\-?\d+$/
retval.push(str.to_i)
else
raise TypeError.new
end
end
return retval end
#
# Take a coeff and an exponent and return the string representation, ignoring
# the sign of the coefficient.
def term_to_str(coef, exp)
ret = ""
# Show coeff, unless it's 1 or at the right
coef = coef.abs
ret = coef.to_s unless coef == 1 & exp > 0
ret += "x" if exp > 0 # x if exponent not 0
ret += "^" + exp.to_s if exp > 1 # ^exponent, if > 1.
return ret end
#
# Create a string of the polynomial in sort-of-readable form.
def polystr(p)
# Get the exponent of first coefficient, plus 1.
exp = p.length
# Assign exponents to each term, making pairs of coeff and exponent,
# Then get rid of the zero terms.
p = (p.map { |c| exp -= 1; [ c, exp ] }).select { |p| p[0] != 0 }
# If there's nothing left, it's a zero
return "0" if p.empty?
# *** Now p is a non-empty list of [ coef, exponent ] pairs. ***
# Convert the first term, preceded by a "-" if it's negative.
result = (if p[0][0] < 0 then "-" else "" end) + term_to_str(*p[0])
# Convert the rest of the terms, in each case adding the appropriate
# + or - separating them.
for term in p[1...p.length]
# Add the separator then the rep. of the term.
result += (if term[0] < 0 then " - " else " + " end) +
term_to_str(*term)
end
return result end
#
# Run until some kind of endfile.
begin
# Repeat until an exception or quit gets us out.
while true
# Read a poly until it works.
An EOF will except out of the
# program.
print "\n"
begin
poly = readints("Enter a polynomial coefficients: ")
rescue TypeError
print "Try again.\n"
retry
end
break if poly.empty?
# Read and evaluate x values until the user types a blank line.
# Again, an EOF will except out of the pgm.
while true
# Request an integer.
print "Enter x value or blank line: "
x = readline.chomp
break if x == ''
raise EOFError.new if x == 'quit'
# If it looks bad, let's try again.
if x !~ /^\-?\d+$/
print "That doesn't look like an integer.
Please try again.\n"
next
end
# Convert to an integer and print the result.
x = x.to_i
print "p(x) = ", polystr(poly), "\n"
print "p(", x, ") = ", polyval(x, poly), "\n"
end
end rescue EOFError
print "\n=== EOF ===\n"
rescue Interrupt, SignalException
print "\n=== Interrupted ===\n"
else
print "--- Bye ---\n"
end
This program reads and evaluates polynomials.
The arithmetic uses integers, though it could be adapted easily enough to used floats outside the exponent values.
Polynomials are read in as a list of coefficients.
The number of values determines the order.
The main program is a double loop,
the outer reading polynomials, and the inner reading x values.
The user can enter any number of polynomials, and, for each, evaluate them at any number of domain values.
Either loop is ended by entering a blank line.
An EOF will halt the program directly.
A couple of novelties and notables:
The polyval
function takes an x value and a list of coefficients representing the polynomial and evaluates it at x.
The evaluation loop is destructive, so the function begins by cloning the array so the original is un-damaged.
The clone method, as you might imagine, makes a copy of the object.
The built-in readline
function is the same as gets
,
except it raises the end-of-file exception.
Ruby gives you a choice to use exceptions or not.
This program makes some use of the list operations.
In particular,
the polystr
method which converts the polynomial to a
string for printing uses map
and select
.
The map
converts the list of coefficients to a list of pairs, coefficient and exponent, then select
eliminates the zero terms.
When the user enters a bad polynomial, the program raises a
TypeError
.
The reading loop catches this with a rescue
, then issues a retry
to allow the user to reenter the polynomial.
The retry
used in a rescue
block simply starts the begin
block over from the beginning.
Ruby Classes
# Class names must be capitalized.
Technically, it's a constant.
class Fred
# The initialize method is the constructor.
The @val is
# an object value.
def initialize(v)
@val = v
end
# Set it and get it.
def set(v)
@val = v
end
def get
return @val
end end
# Objects are created by the new method of the class object.
a = Fred.new(10)
b = Fred.new(22)
print "A: ", a.get, " ", b.get,"\n";
b.set(34)
print "B: ", a.get, " ", b.get,"\n";
# Ruby classes are always unfinished works.
This does not
# re-define Fred, it adds more stuff to it.
class Fred
def inc
@val += 1
end end
a.inc b.inc print "C: ", a.get, " ", b.get,"\n";
# Objects may have methods all to themselves.
def b.dec
@val -= 1
end
begin
b.dec
a.dec rescue StandardError => msg
print "Error: ", msg, "\n"
end
print "D: ", a.get, " ", b.get,"\n";
Box Class
# Box drawing class.
class Box
# Initialize to given size, and filled with spaces.
def initialize(w,h)
@wid = w
@hgt = h
@fill = ' '
end
# Change the fill.
def fill(f)
@fill = f
return self
end
# Rotate 90 degrees.
def flip
@wid, @hgt = @hgt, @wid
return self
end
# Generate (print) the box
def gen
line('+', @wid - 2, '-')
(@hgt - 2).times { line('|', @wid - 2, @fill) }
line('+', @wid - 2, '-')
end
# For printing
def to_s
fill = @fill
if fill == ' '
fill = '(spaces)'
end
return "Box " + @wid.to_s + "x" + @hgt.to_s + ", filled: " + fill
end
private
# Print one line of the box.
def line(ends, count, fill)
print ends;
count.times { print fill }
print ends, "\n";
end end
# Create some boxes.
b1 = Box.new(10, 4)
b2 = Box.new(5,12).fill('$')
b3 = Box.new(3,3).fill('@')
print "b1 = ", b1, "\nb2 = ", b2, "\nb3 = ", b3, "\n\n"
# Print some boxes.
print "b1:\n";
b1.gen
print "\nb2:\n";
b2.gen
print "\nb3:\n";
b3.gen
print "\nb2 flipped and filled with #:\n";
b2.fill('#').flip.gen print "\nb2 = ", b2, "\n"
Inheritance
# Class names must be capitalized.
Technically, it's a constant.
class Fred
# The initialize method is the constructor.
The @val is
# an object value.
def initialize(v)
@val = v
end
# Set it and get it.
def set(v)
@val = v
end
def get
return @val
end
def more(y)
@val += y
end
def less(y)
@val -= y
end
def to_s
return "Fred(val=" + @val.to_s + ")"
end end
# Class Barney is derived from Fred with the usual meaning.
class Barney < Fred
def initialize(x)
super(x)
@save = x
end
def chk
@save = @val
end
def restore
@val = @save
end
def to_s
return "(Backed-up) " + super + " [backup value: " + @save.to_s + "]"
end
end
# Objects are created by the new method of the class object.
a = Fred.new(398)
b = Barney.new(112)
a.more(34)
b.more(817)
print "A: a = ", a, "\n b = ", b, "\n";
a.more(34)
b.more(817)
print "B: a = ", a, "\n b = ", b, "\n";
b.chk
a.more(34)
b.more(817)
print "C: a = ", a, "\n b = ", b, "\n";
b.restore
print "D: a = ", a, "\n b = ", b, "\n";
Setting Variables
# Class names must be capitalized.
Technically, it's a constant.
class Fred
# The initialize method is the constructor.
The @val is
# an object value.
def initialize(v)
@val = v
end
# Set it and get it.
def set(v)
@val = v
end
def to_s
return "Fred(val=" + @val.to_s + ")"
end
# Since a simple access function is so common, ruby lets you declare one
# automatically, like this:
attr_reader :val
# You can list any number of object variables. Separate by commas, and each
# needs its own colon
# attr_reader :fred, :joe, :alex, :sally end
class Alice <Fred
# We have a message, too.
def initialize(n, m)
super(n)
@msg = m
end
# Takes the base result and changes the class name.
def to_s
ret = super
ret.gsub!(/Fred/, 'Alice')
return ret + ' ' + @msg + '!'
end
# The = allows the method to be used on the right, and the left of the
# assignment is the parameter.
def appmsg=(more)
@msg += more
end
# Like attr_reader, if you want the data to be assignable.
attr_writer :msg end
a = Fred.new(45)
b = Alice.new(11, "So there")
print "A: a = ", a, "\n b = ", b, "\n"
print "B: ", a.val, " ", b.val, "\n"
b.msg = "Never"
print "B: b = ", b, "\n"
b.appmsg = " In a million years"
print "C: b = ", b, "\n"
Gate Classes
#
# Ruby circuit simulation classes.
This file contains a base class Gate,
# and several derived classes describing digital logic gates.
There are
# also classes for input and display.
There's also a flip-flop.
#
class Gate
# This is a count of the "active" gates, which are ones which have received
# a signal but not resolved it.
@@active = 0
# This is a list of gates which have registered that they want to be
# notified when the circuit is quiet.
They give an integer priority,
# and are notified in increasing priority order.
@@needquiet = { }
def quiet_register(pri)
if ! @@needquiet.key?(pri) then @@needquiet[pri] = [ ] end
@@needquiet[pri].push(self)
end
# Here's how we set stuff.
There are static and object versions of
# each, since I may want to activate from other spots.
def Gate.activate
@@active += 1
end
def activate
Gate.activate
end
def Gate.deactivate
@@active -= 1
if @@active == 0 then
@@needquiet.keys.sort.each \
{ |p| @@needquiet[p].each { |g| g.onquiet } }
end
end
def deactivate
Gate.deactivate
end
# This is the default quiet action (nothing).
def onquiet
end
# A signal is directed to a particular port on a particular gate.
This
# encapsulates those two data.
When a gate connects to us, we send back
# one of these to direct its later signal changes.
class LinkHandle
def initialize(sink_gate, sink_port)
@sinkg = sink_gate
@sinkp = sink_port
end
# The sending gate uses this method to forward the signal to the
# downstream gate.
def signal(value)
@sinkg.signal(@sinkp,value)
end
attr_reader :sinkg, :sinkp
end
def initialize(ival = false)
@inputs = [ ] # Array of inputs (boolean values)
@outputs = [ ] # Array of LinkHandle objects where to send output
@outval = ival # Present output value.
end
# This is called when a input gate sends us a signal on a particular input.
# We recompute our output value, and, if it changes, we send it on to all
# of our outbound connections.
def signal(port, val)
# The derived class needs to implement the value method.
self.activate
@inputs[port] = val
newval = self.value
if newval != @outval then
@outval = newval
@outputs.each { | c | c.signal(newval) }
end
self.deactivate
end
# Call this to connect your output to the next one of our inputs.
def connect(v)
port = @inputs.length
@inputs.push(v)
c = LinkHandle.new(self, port)
self.signal(port, v)
return c
end
# Join me to another gate.
def join(g)
@outputs.push(g.connect(@outval))
end
def joinmany(*p)
p.each { |i| self.join(i); }
end
attr_reader :outval
# Some printing help
def name
return self.class.to_s
end
def insstr
return (if @inputs.length == 0 then "-" else @inputs.join('.') end)
end
def to_s
return name + " " + insstr + " => " + @outval.to_s
end
# Create another object of the same type.
def another
return self.class.new
end
def manyothers(n)
ret = []
n.times { ret.push(self.another) }
return ret
end
# This manufactures any number of objects.
It is a static method, and
# inherited by the real gates.
The expression self.new, then, runs the
# new method on the actual object, which the inheriting class.
Therefore,
# it will create any gate.
def Gate.many(n)
ret = [ ]
n.times { ret.push(self.new) }
return ret
end
# Dump a whole circuit.
Yecch.
def outlinks
return @outputs
end
def Gate.dump(*roots)
ct = -1
gatemap = { }
for g in roots
gatemap[g] = (ct += 1) unless gatemap.has_key?(g)
end
printed = { }
while roots.length > 0
g = roots.shift
next if printed.has_key?(g)
print "[", gatemap[g], "] ", g, ":"
for c in g.outlinks
og = c.sinkg
gatemap[og] = (ct += 1) unless gatemap.has_key?(og)
print " ", gatemap[og], "@", c.sinkp
roots.push(og)
end
print " [none]" if g.outlinks.length <= 0
print "\n"
printed[g] = true
end
end end
# Standard and gate class AndGate < Gate
def initialize
super(true)
end
def value
for i in @inputs
return false if !i
end
return true
end end class NandGate < AndGate
def value
return ! super
end end
# Standard or gate class OrGate < Gate
def value
for i in @inputs
return true if i
end
return false
end end class NorGate < OrGate
def value
return ! super
end end
# Standard xor gate class XorGate < Gate
def value
ret = false
for i in @inputs
ret ^= i
end
return ret
end end
# Gates with a limited number of input connections.
class LimitedGate < Gate
def initialize(max=1,i=false)
super(i)
@max = max
end
# Enforce connect limit.
def connect(v)
if @inputs.length >= @max then
raise TypeError.new("Too many input connections.")
end
super(v)
end
end
# Not gate.
class NotGate < LimitedGate
def initialize
super(1,true)
end
def value
return ! @inputs[0]
end end
# This is a "yes gate" or amplifier.
It just forwards its input to all its
# outputs class Connector < LimitedGate
def value
return @inputs[0]
end
# We can also use it as a one-bit input device.
def send(v)
self.signal(0,v)
end end
# D Flip-Flop.
Level-triggered.
First input is D, second is clock.
class FlipFlop < LimitedGate
def initialize
super(2)
end
def value
return (if @inputs[1] then @inputs[0] else @outval end)
end end
# D Flip-Flop.
Edge-triggered.
First input is D, second is clock.
# I think the level-triggered might make a lot more sense with this
# simulation, though these are better in circuits.
class FlipFlopET < FlipFlop
def initialize
super
@newval = false
end
def value
return @newval
end
def signal(port, val)
# Need to stick our fingers in this thing to find the rising edge.
self.activate
@newval =
if port == 1 & !@inputs[1] & val then @inputs[0] else @outval end
super(port,val)
self.deactivate
end end
# Simple test point class Tester < LimitedGate
def initialize(name="Tester")
super(1)
@name = name
end
attr_writer :name
def value
print @name, ": ", if @inputs[0] then "on" else "off" end, "\n";
return @inputs[0]
end end
# Numeric output device.
Connect lines starting with LSB.
class NumberOut < Gate
@@quiet = false
def NumberOut.shush(q=true)
@@quiet = q
end
def initialize(name="Value", pri = 1)
@name = name
quiet_register(pri)
super()
end
attr_writer :name
# Print the value on quiet.
def onquiet
return if @@quiet;
val = 0
@inputs.reverse_each { |i|
val <<= 1
if i then
val |= 1
end
}
print @name, ": ", val, "\n"
end
def value
return false
end end
# LED which prints when circuit becomes quiet.
class LED < NumberOut
def initialize(name="LED", pri = 1)
super(name, pri)
end
def onquiet
if @inputs.length > 0 & ! @@quiet then
print @name, ": ", if @inputs[0] then "on" else "off" end, "\n"
end
end
def connect(v)
if @inputs.length >= 1 then
raise TypeError.new("Too many input connections.")
end
super(v)
end end
# Base for input devices.
Mostly deals will collecting connections.
class InputDevice
def initialize
@targs = []
end
# Add a connection
def join(g)
@targs.push(g.connect(false))
end
def joinmany(*p)
p.each { |i| self.join(i); }
end
def outlinks
return @targs
end
end
# Switch bank.
Connects to any number of gates, and will feed them a
# binary number (as a string).
Connections start with LSB.
Initially,
# all the switches are off.
class SwitchBank < InputDevice
# Send a number.
Can take an integer or a string.
def set(n)
if n.is_a?(TrueClass) || n.is_a?(FalseClass) then
@targs.each { | x | x.signal(n) }
elsif n.is_a?(Integer) then
@targs.each { | x | x.signal(n&1 == 1); n >>= 1 }
else
# Assume n is an ascii string of 1's and 0's.
if n.length < @targs.length then
n = ('0' * (@targs.length - n.length)) + n
end
sub = n.length - 1
@targs.each { | x | x.signal(n[sub].chr != "0"); sub -= 1 }
end
end
# This is like switch, but it keeps the circuit active during each
# sending.
def value=(n)
Gate.activate
self.set(n)
Gate.deactivate
end
end
# Send a pulse (clock tick?)
class Pulser < InputDevice
def pulse
Gate.activate
@targs.each { |t| t.signal(true); }
@targs.each { |t| t.signal(false); }
Gate.deactivate
end end
Circuit Test I
require("csim")
S = SwitchBank.new
A = AndGate.new
#A.join(Tester.new("A"))
B = OrGate.new
#B.join(Tester.new("B"))
C = XorGate.new
#C.join(Tester.new("C"))
L = LED.new('Result');
S.join(A)
S.join(A)
A.join(B)
S.join(B)
B.join(C)
S.join(C)
C.join(L)
for x in (0..15)
for s in [3,2,1,0]
print (x>>s) & 1
end
print " => "
S.value = x end
Circuit Test II
require("csim")
S = SwitchBank.new
A = AndGate.new
#A.join(Tester.new("A"))
B = OrGate.new
#B.join(Tester.new("B"))
C = AndGate.new
#C.join(Tester.new("C"))
D = OrGate.new
#D.join(Tester.new("D"))
E = NotGate.new
L = LED.new('Result')
A.join(D)
B.join(D)
C.join(D)
D.join(E)
D.join(A)
D.join(C)
S.join(A)
S.join(B)
S.join(B)
S.join(C)
E.join(L)
for x in (0..15)
for s in [3,2,1,0]
print (x>>s) & 1
end
print " => "
S.value = x end
Circuit Test III
require("csim")
# The circuit here should simulate a level-triggered D flip-flop
D = SwitchBank.new
C = Pulser.new
N = NotGate.new
A1, A2 = AndGate.many(2)
NR1, NR2 = NorGate.many(2)
Q = LED.new(" Q")
Qnot = LED.new(" ~Q")
D.join(N)
N.join(A1)
D.join(A2)
C.join(A1)
C.join(A2)
A1.join(NR1)
A2.join(NR2)
NR1.join(NR2)
NR2.join(NR1)
LED.shush(true)
NR1.join(Q)
NR2.join(Qnot)
LED.shush(false)
vals = [ true, false, false, true, false ]
10.times do
3.times do
dval = vals.shift
vals.push(dval)
print "Input: ", dval, "\n"
D.value = dval
end
print "=== TIC ===\n"
C.pulse end
Subassemblies
require "csim"
# This represents a compound of gates.
It has an interface similar enough
# to Gate to be used in place of one, though it isn't derived from Gate.
# Ruby thinks this is just fine.
# To build compound circuits, you must create a Blueprint.
To do so:
# 1. Create the Blueprint object.
# 2. Create the objects, and connect them to each other.
Don't make
# any external connections.
# 3. As needed specify gates you create as input or output devices for
# the compund.
# 4. Call lock().
This completes the Bluprint and makes it ready to
# generate compounds.
# The resulting Blueprint is also a Compound.
It can be connected and used
# like a Gate (though it is not derived from Gate).
The another method
# of Blueprint creates a Compound, which can be used in a circuit, but
# lacks the building infrastructure.
The Compound remembers its Blueprint,
# and its another method simply calls the one in Blueprint.
#
# You should not add the same component to multiple compounds.
class Compound protected
# This is called only by Blueprint.
Clients don't get to create
# Compounds themselves.
def initialize(ingates, outgates, blueprint)
@ingates = ingates # Initially array of input gates, replaced
# on each connect with a connection to it.
@conndex = -1 # Index for connecting to it.
@outgates = outgates # Array of output gates.
@joindex = -1 # Index for joining.
@blueprint = blueprint # Our blueprint object.
end
public
# Connect to a specific input port.
def portconn(port, v)
c = @ingates[port]
c.signal(v)
return Gate::LinkHandle.new(self,port)
end
# Connect to the next input port.
def connect(v)
return portconn(@conndex += 1, v)
end
# Join an output to another device.
Outputs are joined in the order
# specified by outputs, or out(n) may be used to join to a specific output.
def out(n)
return @outgates[n]
end
def join(gate)
@outgates[@joindex += 1].join(gate)
end
# Handle inbound signals.
def signal(port, val)
Gate.activate
@ingates[port].signal(val)
Gate.deactivate
end
# All the links on the output list, except for internal connections.
def outlinks
ret = [ ]
for i in (0...@outgates.length)
outs = @outgates[i].outlinks
for j in (0...outs.length)
ret.push(outs[j]) if outs[j] & @blueprint.internmap[i][j] != 1
end
end
return ret
end
# This creates another one of us.
This runs in the blueprint, which
# has more information.
def another
return @blueprint.another
end
# Create several others of us in an array.
def manyothers(n)
ret = []
n.times { ret.push(self.another) }
return ret
end
# For prettier printing.
def to_s
ret = self.class.to_s
st = @blueprint.subtype
ret += " [" + st + "]" if st
return ret + " " + self.object_id.to_s
end
end
# Class Blueprint is used to describe a component made of other components.
class Blueprint < Compound
# Note: Initialization is not really complete until the lock method is
# called.
def initialize(subtype = nil)
super([], [], self)
@allgates = [ ] # This is a list of connections to internal gates
# which represent the device inputs.
@allcons = [ ] # List of [src, sink] in proper connect order.
@internmap = [ ] # For each output gate, a mask of connections which
# are internal.
@subtype = subtype
end
# Specify gates as inputs to the circuits.
You may specify any number of
# Gates or Compounds, or arrays thereof.
Each input makes a connection to
# the specified gate in the order given; gates may be repeated when the
# they form more than one input.
If input order matters, be careful to
# mix the specification of gates as inputs, and the joining of internal
# connections, in the correct order.
def inputs(*gates)
for g in flatten(gates)
@ingates.push(g.connect(false))
end
end
# This specifies some gates which are outputs.
Gates, Compounds, or arrays
# thereof may be specified.
Output connections are made to these gates
# in the order given.
Gates may be repeated to supply multiple ouputs.
# It is also possible to use a specific output connection number to
# join multiple devices to the same port.
def outputs(*gates)
@outgates += flatten(gates)
end
# This closes the definition.
It finds all the objects in the collection,
# and makes a list of pairs that can be used to reproduce all the connections
# preserving relative order at each end.
This involves a topological sort.
# Yecccch.
def lock
# Create the initial pending list of all input gates, plus all the
# output gates (which should be redundant, but ...)
gset = { }
((@ingates.map { |c| c.sinkg }) + @outgates).each { |g| gset[g] = true }
pend = gset.keys
# Process pending gates for outlinks.
Find all devices which are
# connected downstream from an input or output.
while g = pend.shift
# Scan the receiving gates.
We take the list of outbound connections
# and extract the sink gates therein, and run through that.
for t in g.outlinks.map { |lnk| lnk.sinkg }
if ! gset.has_key?(t) then
# If not already in the set, add it to the set and the pending list.
pend.push(t)
gset[t] = true
end
end
end
# These are all the reachable gates; all the gates in the device.
@allgates = gset.keys
# Allocate graph nodes for each connection.
This allocates a node for
# each connection between two contained gates.
Each node is an array
# of the form
# [ srcnext, sinknext, predcnt, src, sink ]
# Where the first two point to nodes that come later in the order at
# the source or destination (respectively), predcnt is an integer number
# of nodes which must come before this one, and src and sink are the
# gates at the start and end of the connection.
This loop allocates
# such nodes for each connection, adds links (only) for the source
# ordering, and places them in a hash so we can find them by destination.
nmap = { }
n = 0
for g in @allgates
lnode = nil # Previous node in source ordering.
for c in g.outlinks
# Create the node and store it in the hash.
key = [ c.sinkg, c.sinkp ]
node = [ nil, nil, (if lnode then 1 else 0 end), g, c.sinkg ]
nmap[key] = node
# Add a link here to the previous node.
lnode[0] = node if lnode
lnode = node
end
n += 1
end
# This adds more nodes to represent the array of input links.
These
# nodes are like the above, except source position is nil.
lnode = nil
for c in @ingates do
key = [ c.sinkg, c.sinkp ]
node = [ nil, nil, (if lnode then 1 else 0 end), nil, c.sinkg ]
nmap[key] = node
lnode[0] = node if lnode
lnode = node
end
# Now we go through and add links representing the ordering at the
# destination.
for k in nmap.keys # Scan all node keys.
gate, port = k # Get the destination information
targ = [gate, port+1] # Get key of next node in destination order
if nmap.has_key?(targ) # See if such a node exists.
tnode = nmap[targ] # Get the sink successor from the hash.
tnode[2] += 1 # Increment its pred. count for source node.
nmap[k][1] = tnode # Add a link from source to sink node.
end
end
# Find all the roots (nodes without predecessors).
This is the initial
# value of links which may be established, since all links which must
# appear earlier have been created.
ready = nmap.values.select { |n| n[2] == 0 }
# Traverse the graph obeyong all the constratints and produce a list
# of connections in an order which will preserve the order at each end.
# That preserves the creation order of the links in case it matters.
@allcons = [ ]
while n = ready.shift
# Extract the contents of the ready node and add the relevant information
# to the output order list.
srcnext, sinknext, count, source, sink, sinkp = n
@allcons.push([source, sink])
# Reduce the count of each successor, and add it each to the ready list
# if it has no more predecessors.
if srcnext then
srcnext[2] -= 1;
ready.push(srcnext) if srcnext[2] == 0
end
if sinknext then
sinknext[2] -= 1;
ready.push(sinknext) if sinknext[2] == 0
end
end
# This makes a record of all internal links outbound from output
# devices.
These are not to be reported as connections out from the
# compound.
They are recorded in an array parallel to @outgates.
# For each output gate, they contain a bitmap showing ones for each
# position occupied at this time.
@internmap = @outgates.map do |g|
ret = 0
bit = 1
g.outlinks.each { |c| if c then ret &= bit end; bit <<= 1; }
ret
end
# Whew!
end
# Make a Compound like this one.
def another
# Make copies of all the objects, and keep a map from the original to the
# copy, so we can copy the connections.
copymap = { }
@allgates.each { |g| copymap[g] = g.another }
# Reproduce all the connections on the new gates.
Use the order computed
# by close which preserves order of connections at each end.
ingates = [ ]
for c in @allcons
if c[0] then
copymap[c[0]].join(copymap[c[1]])
else
# nil source indicates an input connection
ingates.push(copymap[c[1]].connect(false))
end
end
# Construct the new compound using the new gates.
return Compound.new(ingates, @outgates.map { |g| copymap[g] }, self)
end
attr_reader :internmap, :subtype
private
# This flattens an list by opening component arrays.
def flatten(a)
ret = []
a.each { |x| if x.is_a?(Array) then ret += x else ret.push(x) end }
return ret
end end
One-Bit Adder Subassembly
#
# This defines a one-bit adder.
#
require "csim"
require "cgrp"
class OBA < Blueprint
def initialize
super("one-bit adder")
# Get the parts.
a,b,cin = Connector.many(3)
gA = XorGate.new
gB,gC,gD = AndGate.many(3)
gE = OrGate.new
# Hook 'em up.
a.joinmany(gA,gB,gC)
b.joinmany(gA,gB,gD)
cin.joinmany(gA,gC,gD)
[gB,gC,gD].each { |g| g.join(gE) }
# Put it into a box.
self.inputs(a,b,cin)
self.outputs(gA, gE)
self.lock
end end
Eight-BIt Adder
#
# This is a one-bit adder.
#
require "csim"
require "cgrp"
require "oba"
NumberOut.shush
# Blueprint for a the one-bit adder bp = OBA.new
# Two input senders, and the output device.
na = SwitchBank.new nb = SwitchBank.new disp = NumberOut.new(" Sum")
# We're going to build an 8-bit adder prev = nil
8.times do
# Create the one-bit adder and join the data inputs and outputs.
addr = bp.another
na.join(addr)
nb.join(addr)
addr.join(disp)
# Chain the carry, if this isn't he first one.
if prev then
prev.join(addr)
end
prev = addr end
# Overflow light.
prev.join(LED.new(" Oflow"))
NumberOut.shush(false)
30.times do
a = rand(256)
b = rand(256)
print a, " + ", b, ":\n"
Gate.activate
na.value = a
nb.value = b
Gate.deactivate end
N-Bit Adder Subassembly
#
# An N-bit adder built of one-bit adders with simple carry propagation.
#
require "oba"
class Adder < Blueprint
# This initializes the object, If we already have a one-bit adder
# blueprint lying around, we can reuse it with that second argument.
def initialize(n, bp = nil)
super(n.to_s + "-bit adder")
# Blueprint for a the one-bit adder
bp = OBA.new unless bp
@one_bit_bp = bp
# Make all the adders, and specify them as inputs.
addrs = bp.manyothers(n)
self.inputs(addrs,addrs)
# Create output connectors and chain the carries.
prev = nil
for addr in addrs
c = Connector.new
addr.join(c)
if prev then
prev.out(1).join(addr)
end
self.outputs(c)
prev = addr
end
# Overflow
self.outputs(prev)
self.lock
end
attr_reader :one_bit_bp end
Adder Test
#
# This tests the adder simulation by building various size adders and running
# them various numbers of times.
#
require "csim"
require "adder"
# Size of the adder.
size = 8
rpt = 20
size = $*[0].to_i if $*.length > 0
rpt = $*[1].to_i if $*.length > 1
NumberOut.shush
# Blueprint for the 10-bit adder.
bp = Adder.new(size)
addr = bp.another
# Hook up two input switch banks, one output, and an overflow led.
na = SwitchBank.new size.times { na.join(addr) }
nb = SwitchBank.new size.times { nb.join(addr) }
disp = NumberOut.new(" Sum")
size.times { addr.join(disp) }
addr.join(LED.new(" Oflow"))
#Gate.dump(na)
NumberOut.shush(false)
# Perform 30 addtions of random numbers.
For each, we choose two random
# inputs, and set them into the input switch banks.
The outputs will print,
# being the sum numeric display and the overflow LED.
print "== Performing #{rpt} tests on #{size}-bit adder.
",
"Max sum is #{(1<<size)-1} ==\n"
rpt.times do
# Choose randoms and print them.
a = rand(1<<size)
b = rand(1<<size)
print a, " + ", b, ":\n"
# Add them, and keep the circuits active to suppress printout until after
# the whole operation is complete.
Gate.activate
na.value = a
nb.value = b
Gate.deactivate end
Unit Conversion
This is a lot of code to demonstrate something simple.
The module is a convenient place to group defintions which work together.
In this case, to perform unit conversions.
The next file is a short driver which uses this module.
#
# This module provides unit conversion.
The main public name is
# the method ratio, which gives the ratio of two dimensioned expressions,
# or throws an exception if they do not conform.
The exceptions are also
# public, as are the BaseUnit and RelatedUnit classes, which can be used
# to add conversion information to the module.
#
# Units.ratio(from, to)
# Will tell you how many to you need to equal from.
From and to are strings
# of the form
# [ n ] { [ / ] unitname[ ^r ] }+
# Where n is the number of whatever units, defaulting to 1.0.
The unitname
# is a unit or alias created with BaseUnit or RelatedUnit.
Unitnames may
# be separated by spaces or dashes.
Power on the unit defaults to 1.
module Units public
# Two new exceptions just for us to pitch.
class UnitsException < Exception
# Base class for exceptions invented for this module.
end
# A unit expression string couldn't be parsed.
class UnitParseError < UnitsException
def initialize(m = "Cannot parse measurement expression")
super(m)
end
end
# Conversion of units which are not conformable (feet to liters or like
# that).
class UnitConformability < UnitsException
def initialize(m = "Units not conformable")
super(m)
end
end
# Abstract base class for units.
class Unit
# A list of all the units we know.
@@units = { }
# Access to @@units.
def Unit.exists(n)
return @@units.has_key?(n)
end
def Unit.named(n)
return @@units[n]
end
# If this were Java, I'd define an abstract function isbase() which tells
# if this object is a BaseUnit or not.
def initialize(name)
@name = name
@@units[name] = self
@@units[name + 's'] = self
end
attr_reader :name
def alias(*names)
names.each { |n| @@units[n] = self }
end
end
# This is the base unit class.
There is one base unit for each
# dimension.
Any unit in the right dimension could be used.
# The class is just a name and a dimension name.
class BaseUnit < Unit
def isbase()
return true
end
def initialize(name, dname)
super(name)
@dimension = dname
end
attr_reader :dimension
# This orders the unit objects arbitrarily, but that is sufficient to
# sort them and compare lists.
def <=> (u)
return object_id <=> u.object_id
end
end
private
# Here are the base units for each dimension.
BaseUnit.new("meter", "length").alias("m", "metre")
BaseUnit.new("gram", "mass").alias("g")
BaseUnit.new("second", "time").alias("s", "sec")
# A measurement is a number and some numerator and denominator units.
# The unit lists are kept in lowest terms of base units, though the object
# may be initialized with any units.
class Measurement
def initialize(m, num, denom)
@mult = m.to_f # The multiplier.
to_f in case you send an integer.
@num = num # The numerator units
@denom = denom # The denominator units.
normalize # Convert to lowest terms of base units.
end
attr_reader :mult, :num, :denom
# Return the ratio.
Throws conformability.
def ratio(divby)
raise UnitConformability.new \
if @num != divby.num || @denom != divby.denom
return @mult / divby.mult
end
private
# Convert to base units only, in lowest terms.
def normalize
# Convert the lists to just base units.
newnum = []
newdenom = []
basify(@num, false, newnum, newdenom)
basify(@denom, true, newdenom, newnum)
# Now eliminate units which appear in both places.
This depends on
# an arbitrary ordering of the base units which allows us to compare
# them in a merge order.
newnum.sort!
newdenom.sort!
@num = [ ]
@denom = [ ]
while newnum.length > 0 & newdenom.length > 0
rel = newnum[0] <=> newdenom[0]
if rel < 0
# They are different and the num comes first.
Must be kept.
@num.push(newnum.shift)
elsif rel > 0
# They are different and the denom comes first.
Must be kept.
@denom.push(newdenom.shift)
else
# A match.
Eliminate.
newnum.shift
newdenom.shift
end
end
@num.concat(newnum)
@denom.concat(newdenom)
end
# Convert the source list to base, adding the components to ndest and
# ddest, and multiplying or dividing the measure's number.
The
# src is a unit list.
These may not be base units, but, if not, the
# measures they contain will be.
def basify(src, divide, ndest, ddest)
for unit in src
if unit.isbase
ndest.push(unit)
else
@mult = if divide then
@mult / unit.related.mult
else
@mult * unit.related.mult
end
ndest.concat(unit.related.num)
ddest.concat(unit.related.denom)
end
end
end
end
# This can be called as (qty, unitex) or just (unitex), where m is
# taken as 1.0.
Units expression is name[^pwr] [ / name... ]
# unit names are always alpha.
- is a separator like a space, but not
# between ^ and pwr.
def Units.qty(m, s=nil)
m,s = 1.0,m if s == nil
# See if there's a number at the front of the expression.
if s.sub!(/^\s*(\-?(\d+(\.\d*)?|\d*\.\d+))\s*/, '')
m *= $1.to_f
end
# Collect the stuff needed for the units part.
num = []
denom = []
# What's the next thing?
puthere = num
otherone = denom
while true
# Strip leading crud, which is spaces or dashes
s.sub!(/^(\s|\-)*/, '')
break if s == ''
# Find the next "thing", which is a unit name or a /.
s.sub!(%r=^([a-zA-Z]+|/)=, '') or
raise UnitParseError.new('Expected unit name or slash at "' + s + '"' )
thing = $1
if thing == '/'
# Swap which list the units go into.
puthere, otherone = otherone, puthere
else
# Unit name.
Unit.exists(thing) or
raise UnitParseError.new('Unknown unit name "' + thing + '"')
unit = Unit.named(thing)
# See if there's a ^n
ct = 1
top = true
if s.sub!(/^\s*\^\s*(\-?)(\d+)/, '')
ct = $2.to_i
top = false if $1 != ''
end
if top
ct.times { puthere.push(unit) }
else
ct.times { otherone.push(unit) }
end
end
end
return Measurement.new(m, num, denom)
end
public
# Return the ratio.
def Units.ratio(top, bot)
return Units.qty(top).ratio(Units.qty(bot))
end
# The units that are not basic
class RelatedUnit < Unit
def isbase()
return false
end
def initialize(name, measure)
super(name)
measure = Units.qty(measure) if measure.kind_of?(String)
@related = measure
end
attr_reader :related
end
private
# Here are all the rest of the units we know about.
RelatedUnit.new("kilometer", "1000 meter").alias("km")
RelatedUnit.new("centimeter", "0.01 meter").alias("cm")
RelatedUnit.new("milimeter", "0.01 meter").alias("mm")
RelatedUnit.new("inch", "2.54 cm").alias("in")
RelatedUnit.new("foot", "12 in").alias("ft", "feet")
RelatedUnit.new("mile", "5280 ft").alias("mi")
RelatedUnit.new("yard", "3 ft").alias("yd")
RelatedUnit.new("furlong", "660 ft")
RelatedUnit.new("milliliter", "cm^3").alias("ml", "cc")
RelatedUnit.new("liter", "1000 ml").alias("l")
RelatedUnit.new("gallon", "3.785412 liter").alias("gal")
RelatedUnit.new("quart", "0.25 gal").alias("qt")
RelatedUnit.new("pint", "0.5 quart").alias("pt")
RelatedUnit.new("cup", "0.25 quart")
RelatedUnit.new("acre", "43560 ft^2")
RelatedUnit.new("hectare", "10000 m^2")
RelatedUnit.new("minute", "60 sec").alias("min")
RelatedUnit.new("hour", "60 min").alias("hr")
RelatedUnit.new("day", "24 hr")
RelatedUnit.new("week", "7 day").alias("wk")
RelatedUnit.new("fortnight", "14 day")
RelatedUnit.new("year", "365.25 day").alias("yr")
RelatedUnit.new("kilogram", "1000 gram").alias("kg")
RelatedUnit.new("slug", "14.593903 kg")
RelatedUnit.new("newton", "kg-m/s^2").alias("N")
RelatedUnit.new("pound", "4.448222 N").alias("lb")
RelatedUnit.new("joule", "N-m").alias("J")
RelatedUnit.new("calorie", "0.238846 J").alias("cal")
RelatedUnit.new("kcal", "1000 cal")
RelatedUnit.new("BTU", "1055.055853 J")
RelatedUnit.new("watt", "J/s")
RelatedUnit.new("kilowatt", "1000 watt").alias("kw")
RelatedUnit.new("horsepower", "746 watt")
RelatedUnit.new("knot", "1.68781 ft/sec")
end
Unit Conversion Driver
#
# This is a small program which uses the Units module to perform conversions
# requested by the user.
require("units")
print "You have: "
while from = gets
from.chomp!
print "You want: "
to = gets.chomp
begin
result = Units.ratio(from.clone, to.clone)
print from, " = ", result, " ", to, "\n"
rescue Exception
# Stupidly enough, Exception isn't the default; StandardError is, which is
# a subclass of Exception.
print "Failed: #{$!}\n"
end
print "You have: "
end
Inclusion Modules
# The two small modules here are intended to contain generic facilities
# which can be used by classes.
# This follows using the next method until we get to the end of whatever it
# is.
module Follower
def last
at = self
while true
n = at.next
if n == nil then return at end
at = n
end
end end
# This prints on one line using the each method.
module Printer
def pr(newline = false)
self.each { |x| print x, " " }
print "\n" if newline
end end
Modules can be used to provide generic facilities using the include facility.
The methods in the modules (well, one each in this case) can be added to classes using the include keyword.
See the next page for its use.
Linked List
require("last")
# Here is a linked list class.
Since there's not much point in writing
# such a class when you already have all the Ruby data structures
# available, you might have figured out it's here to demonstrate something:
# including a module.
#
# A linked list class List
# Nodes for the linked list.
class Node
# Get the last facility which scans to the end of the list.
include Follower
def initialize(d, n = nil)
@val = d
@next = n
end
attr_reader :next, :val
attr_writer :next
end
# Get the printing facility.
include Printer
# Create the list with its first node.
def initialize(first)
@head = Node.new(first)
end
# Add at the front.
We can only add, and the list is created with one
# node, so no special case for empty list.
How nice.
def at_front(v)
n = Node.new(v)
n.next = @head
@head = n
end
# Add to the end of the list.
def at_end(v)
n = Node.new(v)
@head.last.next = n
end
# Process each member of the list.
The yield operator calls the block
# sent to the function.
def each
p = @head
while p != nil
yield p.val
p = p.next
end
end end
See:Programming Ruby
The List class, and Node class inside it, use the include directive to add methods from the Follower
and Printer
modules, respectively.
Note the implementation of the iterator for List
: the
yield
operation runs the code block provided to the iterator.
Tree
require("last")
# A BST class.
Again, the purpose is a demo, not because you need one when
# you already have the Ruby datatypes.
class Tree
class Node
# Again, we include the follower class.
include Follower
def initialize(d)
@val = d
@lft, @rgt = nil
end
attr_reader :lft, :rgt, :val
attr_writer :lft, :rgt
# Our next function just moves right.
(See below)
def next
return @rgt
end
# Insert a new node into the subtree rooted here.
def insert(new)
if new.val < @val then
if @lft == nil then
@lft = new
else
@lft.insert(new)
end
else
if @rgt == nil then
@rgt = new
else
@rgt.insert(new)
end
end
end
# This runs for each value in the tree in sorted order.
The block
# parameter is an object known as a closure.
They contain executable
# code, and blocks can become closures (see below).
def each(block)
if @lft then @lft.each(block) end
block.call(@val)
if @rgt then @rgt.each(block) end
end
end
# Get the printing facility.
include Printer
def initialize(first)
@root = Node.new(first)
end
# Insert a value.
Most of the work in Node#insert
def insert(v)
@root.insert(Node.new(v))
end
# Stepping right from the root until nil gives the max value in the tree.
def max
return @root.last.val
end
# The &blk notation converts the block used in the iterator into a
# closure object, which we send to Node#each.
def each(&blk)
@root.each(blk)
end end
See:Programming Ruby
The Tree
class also includes the common operations.
This one also implements an each
iterator.
This one uses the parameter &blk
.
The &
converts the code block to a Closure object.
Closures are executable code.
It is bound to the parameter blk
which is sent to Node#each
.
There it becomes a parameter block
, and is run with the call
method.
Driver
require("list")
require("tree")
print "=== List test ===\n"
x = List.new(10)
x.at_front(33)
x.at_front(28)
x.at_end(12)
x.at_front(3)
x.at_end(71)
x.pr(true)
s = 0
x.each { |n| s += n }
print "sum = ", s, "\n"
print "\n=== Tree test ===\n"
t = Tree.new(28)
t.insert(38)
t.insert(1)
t.insert(39)
t.insert(17)
t.insert(22)
t.insert(8)
t.insert(11)
t.pr(true)
s = 0
t.each { |n| s += n }
print "sum = ", s, "\n"
print "Max is ", t.max, "\n"
Button
#!/usr/bin/ruby
# Import the library.
require 'tk'
# Root window.
root = TkRoot.new { title 'Push Me' }
# Add a label to the root window.
lab = TkLabel.new(root) { text "Push the Button" }
# Make it appear.
lab.pack
# Here's a button.
Also added to root by default.
TkButton.new {
text "PUSH"
command { print "Arrrrrrg!\n" }
pack
}
Tk.mainloop
Colors
#!/usr/bin/ruby
# Import the library.
require 'tk'
# Root window.
root = TkRoot.new { title 'Push Me'
background '#111188'
}
# Add a label to the root window.
lab = TkLabel.new(root) {
text "Push the Button"
background '#3333AA'
foreground '#CCCCFF'
}
# Make it appear.
lab.pack('side' => 'left', 'fill' => 'y')
# Here's a button.
Also added to root by default.
TkButton.new {
text "PUSH"
background '#EECCCC'
activebackground '#FFEEEE'
foreground '#990000'
command { print "Arrrrrrg!\n" }
pack('side' => 'right')
}
Tk.mainloop
Configure
#!/usr/bin/ruby
# Import the library.
require 'tk'
# Root window.
root = TkRoot.new {
title 'Push Me'
background '#111188'
}
# Add a label to the root window.
lab = TkLabel.new(root) {
text "Hey there,\nPush a button!"
background '#3333AA'
foreground '#CCCCFF'
}
# Make it appear.
lab.pack('side' => 'left', 'fill' => 'both')
# A frame can be used to arrange buttons with the packer.
fr = TkFrame.new fr.pack('side' => 'right', 'fill' => 'both')
# Here's a button.
Added to the frame, not the root.
swapbut = TkButton.new(fr) {
text "Swap"
background '#EECCCC'
activebackground '#FFEEEE'
foreground '#990000'
pack('side' => 'top', 'fill' => 'both')
}
# Another button stopbut = TkButton.new(fr) {
text "Exit"
background '#CCEECC'
activebackground '#EEFFEE'
foreground '#009900'
command { exit }
pack('side' => 'bottom', 'fill' => 'both')
}
# Switch button colors.
def cswap(b1, b2)
# Swap each color between the two buttons.
for loc in ['background', 'foreground', 'activebackground']
c = b1.cget(loc)
b1.configure(loc => b2.cget(loc))
b2.configure(loc => c)
end end
swapbut.configure( 'command' => proc { cswap(swapbut, stopbut) } )
Tk.mainloop
Inheritance
#!/usr/bin/ruby
# Import the library.
require 'tk'
# Root window.
root = TkRoot.new {
title 'Push Me'
background '#111188'
}
# Add a label to the root window.
lab = TkLabel.new(root) {
text "Hey there,\nPush a button!"
background '#3333AA'
foreground '#CCCCFF'
}
# Make it appear.
lab.pack('side' => 'left', 'fill' => 'both')
class TwoLabs < TkFrame
# Switch button colors.
def cswap
# Swap each color between the two buttons.
for loc in ['background', 'foreground', 'activebackground']
c = @swapbut.cget(loc)
@swapbut.configure(loc => @stopbut.cget(loc))
@stopbut.configure(loc => c)
end
end
def initialize
super
# Here's a button.
I can't get the command setting to work
# inside the block, since the self (apparently) becomes the TkButton,
# not us.
@swapbut = TkButton.new(self, 'command' => proc { self.cswap } ) {
text "Swap"
background '#EECCCC'
activebackground '#FFEEEE'
foreground '#990000'
pack('side' => 'top', 'fill' => 'both')
}
# Another button
@stopbut = TkButton.new(self) {
text "Exit"
background '#CCEECC'
activebackground '#EEFFEE'
foreground '#009900'
command { exit }
pack('side' => 'bottom', 'fill' => 'both')
}
end end
# A frame can be used to arrange buttons with the packer.
tl = TwoLabs.new tl.pack('side' => 'right', 'fill' => 'both')
Tk.mainloop
Reflex Test
#!/usr/bin/ruby
# Import the library.
require 'tk'
# Parameters.
Width = 5 # Width of button grid.
Height = 5 # Height of button grid.
MinWait = 200 # Smallest button change wait (ms)
MaxWait = 1400 # Largest button change wait (ms)
InitWait = 800 # Initial button change wait (ms)
LossRate = 2000 # Frequency to take away points.
# Set defaults.
Some we keep in constants to use later.
BG = '#ccffcc'
TkOption.add('*background', BG)
TkOption.add('*activeBackground', '#ddffdd')
FG = '#006600'
TkOption.add('*foreground', FG)
TkOption.add('*activeForeground', FG)
TkOption.add('*troughColor', '#99dd99')
# Root window.
root = TkRoot.new('background' => BG) { title 'Click Fast' }
# Button from the panel class PanelButton < TkButton private
# Exchange colors on the button.
def cswap
for p in [['background', 'foreground'],
['activebackground', 'activeforeground']]
c = cget(p[0])
configure(p[0] => cget(p[1]))
configure(p[1] => c)
end
end public
# Initialize the button within the widget sup, at position pos (zero-based)
# with the number num.
When pressed, send the score (+ or -) to cmd.
# Scorekeeper is an object which implements an up and down methods to
# receive score changes.
def initialize(sup, pos, num, scorekeeper)
super(sup, 'text' => num.to_s, 'command' => proc { self.pushed },
'activeforeground' => '#990000', 'activebackground' => '#ffdddd')
grid('row' => pos / Width + 1, 'column' => pos % Width, 'sticky' => 'news')
@active = false
@scorekeeper = scorekeeper
end
attr_reader :active
# Activate or deactivate the button.
def activate
if not @active
cswap
@active = true
end
end
def deactivate
if @active
cswap
@active = false
end
end
# When pushed, send our number, or negative our number, to the scorekeeping
# command.
def pushed
n = self.cget('text').to_i
if @active
@scorekeeper.up(n)
else
@scorekeeper.down(n)
end
end end
# This class calls reduces the score at the indicated time rate.
class ScoreTimer
# This object will call scorekeeper.down(step) each rate ms.
def initialize(scorekeeper, rate = 500, step = 1)
@scorekeeper = scorekeeper
@rate = rate
@step = step
Tk.after(rate, proc { self.change })
end
# Reduce the score periodically
def change
@scorekeeper.down(@step)
Tk.after(@rate, proc { self.change })
end end
# This is a box displaying a count-up timer in minutes and seconds to tenths
# m:ss.d class TimeCounter < TkLabel
# Initialize.
Displays zero and starts the ticking event.
def initialize(root)
super(root, "text" => '0:00.0', 'anchor' => 'e')
@count = 0
Tk.after(100, proc { self.change })
end
# One clock tick (tenths of a second).
Increment the counter, then build
# the new display value.
def change
@count += 1
self.configure('text' =>
sprintf("%d:%02d.%d",
@count / 600, (@count / 10) % 60, @count % 10))
Tk.after(100, proc { self.change })
end end
# This is the main application GUI.
class App private
# Set the score value.
def setscore(val)
color = if val < 0 then 'red' else FG end
@slab.configure('text' => val.to_s, 'foreground' => color)
end public
# The wait attribute is the amount of time (ms) between button changes.
attr_writer :wait
# Initialize it and have the applicate drawn in the root window.
def initialize(root)
# This is the label containing the score.
Initially zero.
@slab = TkLabel.new(root) {
text "0"
anchor 'e'
grid('row' => 0, 'column' => 0, 'columnspan' => Width / 2,
'sticky' => 'w')
}
# This is the timer window at upper right.
TimeCounter.new(root).
grid('row' => 0, 'column' => Width/2, 'columnspan' => (Width+1)/2,
'sticky' => 'e')
# Create the buttons.
First, make an array of numbers from 1 to the
# number of buttons, then create the buttons, each labelled with a
# number chosen at random from the list, so thare are no repeats.
nums = (1..Height*Width).to_a;
@buts= [ ]
for n in (0...Height*Width)
pos = rand(nums.length)
@buts.push(PanelButton.new(root, n, nums[pos], self))
nums.delete_at(pos)
end
# This creates the slider to adjust the speed of the game.
The proc is
# called whenever the slider changes, and is sent the new setting.
scale = TkScale.new('command' => proc { |v| self.wait = v.to_i } ) {
orient "horizontal" # Which way the slider goes.
from MinWait # Value of smallest setting
to MaxWait # Value of largest setting
showvalue false # Don't show the numeric value of the setting.
grid('row' => Height + 1, 'column' => 1, 'columnspan' => Width - 2,
'sticky' => 'news')
}
scale.set(InitWait)
# Labels by the slider.
TkLabel.new {
text "Fast"
anchor "w"
grid("row" => Height + 1, 'column' => 0, 'sticky' => 'w')
}
TkLabel.new {
text "Slow"
anchor "e"
grid("row" => Height + 1, 'column' => Width-1, 'sticky' => 'e')
}
@wait = InitWait
# Decrement the score every LossRate period.
@timer = ScoreTimer.new(self, LossRate)
self.change
end
# Actions to increase or decrease the score.
def up(delta)
setscore(@slab.cget('text').to_i + delta)
end
def down(delta)
setscore(@slab.cget('text').to_i - delta)
end
# Change (or set, if none is yet set) the active button.
It deactivates
# the button in @buts[0], It then chooses some other button at random,
# activates that and swaps it into position 0.
def change
@buts[0].deactivate
pos = rand(@buts.length - 1) + 1
@buts[0], @buts[pos] = @buts[pos], @buts[0]
@buts[0].activate
Tk.after(@wait, proc { self.change })
end end
a = App.new(root)
Tk.mainloop
Host Lookup
#
# Host name lookup widget.
#
require 'tk'
require 'socket'
# Set colors.
BG = '#AAAAFF'
TkOption.add('*background', BG)
TkOption.add('*activeBackground', '#CCCCFF')
TkOption.add('*foreground', '#884400')
# A label which does the needed lookup.
class HostnameLabel < TkLabel
# Look up host name in the assocated entry widget source, and display
# in ourselves.
def show
hn = @source.get.strip
if hn == ''
ip = ''
else
begin
ip = IPSocket.getaddress(hn)
rescue
ip = '[unknown]'
end
end
configure('text' => ip)
end
# Create the widget, and bind the return key to run the lookup method (show).
def initialize(root, entry)
super(root, 'text' => '', 'width' => 15)
@source = entry
entry.bind('Return', proc { self.show })
end
end
# Root window root = TkRoot.new('background' => BG) { title 'Host Conversion' }
# Title label tit = TkLabel.new {
text "Host Name Conversion"
relief 'groove'
grid('row' => 0, 'column' => 0, 'columnspan' => 2, 'sticky' => 'news')
}
# Name entry.
entr = TkEntry.new {
width 25
grid('row' => 1, 'column' => 0, 'columnspan' => 2, 'sticky' => 'news')
}
dislab = nil # This needs to exist since we refer to it in the bind.
entr.bind('Button-1', proc { |e|
entr.delete(0,'end')
dislab.configure('text' => '')
})
# Reporting label.
dislab = HostnameLabel.new(root, entr)
dislab.grid('row' => 2, 'column' => 0, 'sticky' => 'news')
# Go button.
but = TkButton.new {
text "Find"
command { dislab.show }
grid('row' => 2, 'column' => 1, 'sticky' => 'news')
}
Tk.mainloop
Bouncing Balls
#!/usr/bin/ruby
#
# This creates a simple animation of five balls bouncing around inside a
# rectagle.
Balls bounce off the sides, but pass through each other.
Nothing
# fancy.
#
# Import the library.
require 'tk'
# Dot diameter.
Diameter = 10
# Update rate (ms).
Frequency = 25
# Canvas size.
Width = 400
Height = 300
# Set defaults.
Some we keep in constants to use later.
BG = '#ccccff'
TkOption.add('*background', BG)
# Root window.
root = TkRoot.new('background' => BG) { title 'Bouncy, Bouncy' }
# Canvas.
c = TkCanvas.new(root) {
width Width
height Height
pack('fill' => 'both')
}
# This is the circle that wanders around the canvas.
class MovingCircle < TkcOval
# Create with a moving circle on the canvas c with indicated color.
def initialize(c, color)
# Remember the canvas.
@canv = c
# Choose an initial location at random and create the object there.
@xpos = rand(Width - Diameter)
@ypos = rand(Height - Diameter)
super(c, @xpos, @ypos, @xpos + Diameter, @ypos + Diameter, 'fill' => color)
# Chose a velocity at random.
1 to 3 pixels per Frequency in each
# dimension.
@delx = (rand(3)+1)*(if rand(2) == 1 then 1 else -1 end)
@dely = (rand(3)+1)*(if rand(2) == 1 then 1 else -1 end)
# Start moving
Tk.after(Frequency, proc { self.move } )
end
# This adjusts a single dimension by one step, taking account of the
# walls.
Send a position, increment, and limit, and get back a new pos
# and new increment (which might have its sign changed).
def del(pos, inc, limit)
# Move
pos += inc
# See if we hit the top or left, and reverse.
if pos < 0 then
pos = -pos
inc = -inc
# Likewise check for hitting the right or bottom
elsif pos > limit - Diameter then
pos = (limit - Diameter) - (pos - (limit - Diameter))
inc = -inc
end
# Send back the results.
return pos, inc
end
# Move one step, then schedule the next move.
def move
# Remember current position, and compute the new one.
oldx, oldy = @xpos, @ypos
@xpos, @delx = del(@xpos, @delx, Width)
@ypos, @dely = del(@ypos, @dely, Height)
# Tell Tk about it.
@canv.move(self, @xpos - oldx, @ypos - oldy)
Tk.after(Frequency, proc { self.move } )
end end
# Make several balls of different color.
for color in [ '#FF9999', '#99FFFF', '#005588', '#992211', '#FF0055' ]
MovingCircle.new(c, color)
end
Tk.mainloop
Binding and Actions
#
# This shows a potential problem in providing button actions.
The code blocks
# are run in the context of the caller, but not until the button is pressed.
# Therefore variable values are current as of the button press.
This is not
# always what you want.
#
require 'tk'
# Root window root = TkRoot.new
# Three buttons created in a loop, numbering from 1 to 3.
bnum = 1
for fred in [ 17, 348, -48 ]
TkButton.new(root) {
text "Button " + bnum.to_s
command proc { print "Button ", bnum, ", fred = ", fred, "\n" }
grid('column' => 0, 'row' => bnum-1, 'sticky' => 'news')
}
bnum += 1
end
# This holds and prints two values.
The parms are evaluated when the
# object is created, so we get the right values.
The button command
# will use any object which understands the call method.
class Printer
def initialize(num, fred)
@num = num
@fred = fred
end
def call
print "Button ", @num, ", fred = ", @fred, "\n"
end end
# Three more buttons, but this time they use the Printer class and work
# correctly.
for fred in [ 'Dogbert', 97, 111 ]
TkButton.new(root) {
text "Button " + bnum.to_s
command Printer.new(bnum, fred)
grid('column' => 1, 'row' => bnum-4, 'sticky' => 'news')
}
bnum += 1
end
# This is a more generic solution class Runner
# Set a proc and some args.
def initialize(method, *args)
@method = method
@args = args
end
# Run it with the args.
def call
@method.call(*@args)
end end
# Three more, but this time they work correctly.
for fred in [ 86, 12, 123 ]
TkButton.new(root) {
text "Button " + bnum.to_s
command Runner.new(proc { |n, f|
print "Button ", n, ", fred = ", f, "\n" },
bnum, fred)
grid('column' => 2, 'row' => bnum-7, 'sticky' => 'news')
}
bnum += 1
end
# These are sort of button-like, but use binding.
lnum = 1
for fred in [ 2973, 'Nosebleed', 349 ]
b = TkLabel.new(root) {
text "Label " + lnum.to_s
grid('column' => 3, 'row' => lnum-1, 'sticky' => 'news')
relief 'raised'
background '#999999'
padx 10
}
b.bind('Button-1',
proc { print "Label ", lnum, ", fred = ", fred, "\n" })
lnum += 1
end
# This shows a nice feature of bind which allows extra parameters to be
# sent in.
for fred in [ 98733, 128, 'Norbert' ]
b = TkLabel.new(root) {
text "Label " + lnum.to_s
grid('column' => 4, 'row' => lnum-4, 'sticky' => 'news')
relief 'raised'
background '#999999'
padx 10
}
b.bind('Button-1',
proc { | n, f | print "Label ", n, ", fred = ", f, "\n" }, lnum, fred)
lnum += 1
end
Tk.mainloop
FTP Download Tool
#!/usr/bin/ruby
require 'tk'
require 'net/ftp'
# Close the connection and terminate pgm.
def term(conn)
if conn
begin
conn.quit
ensure
conn.close
end
end
exit end
# Display an error dialog.
def thud(title, message)
Tk.messageBox('icon' => 'error', 'type' => 'ok',
'title' => title, 'message' => message)
end
# This is the login window.
It pops up and asks for the remote host and the
# user credentials, and a button to initiate the login when the fields are
# ready.
class LoginWindow
# Generate s label/entry pair for the login window.
These will be
# appropriately gridded on row row inside par.
Text box has width
# width and places its contents into the reference $ref.
If $ispwd,
# treat it as a password entry box.
Returns the text variable which
# gives access to the entry.
def genpair(row, text, width, ispwd=false)
tbut = TkLabel.new(@main, 'text' => text) {
grid('row' => row, 'column' => 0, 'sticky' => 'nse')
}
tvar = TkVariable.new('')
lab = TkEntry.new(@main) {
background 'white'
foreground 'black'
textvariable tvar
width width
grid('row' => row, 'column' => 1, 'sticky' => 'nsw')
}
lab.configure('show' => '*') if ispwd
return tvar
end
# Log into the remote host.
If successful, start the directory loader.
# Modes are: 1: Anonymous, 2: User, 3: Return, which does anon if the
# user infor was not filled in, and user otw.
def do_login(mode)
host = @host.value
acct = @acct.value
password = @password.value
# Adjust user data by mode.
if mode == 1 || (mode == 3 & acct == "" & password == "")
acct ='anonymous'
if password == ""
password = 'anonymous'
end
end
# Make sure we're all filled in.
if host == "" || acct == "" || password == ""
thud('No Login Info',
"You must provide a hostname and login credentials.")
return
end
# Attempt to connect to the remote host and log in
begin
@conn = Net::FTP.new(host, acct, password)
@conn.passive = true
rescue
thud("Login Failed", $!)
@conn = nil
return
end
# Display the listing in the window.
@listwin.setconn(@conn)
@main.destroy()
end
def initialize(main, listwin, titfont, titcolor)
@main = TkToplevel.new(main)
@main.title('FTP Login')
# Listing window.
@listwin = listwin
@conn = nil
# This counts through the rows, which makes it easier to modify
# the program.
row = -1
# Label at the top of window.
toplab = TkLabel.new(@main) {
text "FTP Server Login"
justify 'center'
font titfont
foreground titcolor
grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2,
'sticky' => 'news')
}
# Hostname entry
@host = genpair(row += 1, 'Host:', 25)
# Login buttons, in a frame for layout.
bframe = TkFrame.new(@main) {
grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2,
'sticky' => 'news')
}
TkButton.new(bframe, 'command' => proc { self.do_login(1) }) {
text 'Anon. Login'
pack('side' => 'left', 'expand' => 'yes', 'fill' => 'both')
}
TkButton.new(bframe, 'command' => proc { self.do_login(2) }) {
text 'User Login'
pack('side' => 'left', 'expand' => 'yes', 'fill' => 'both')
}
# Login and password entries.
@acct = genpair(row += 1, 'Login:', 15)
@password = genpair(row += 1, 'Password:', 15, true)
stop = TkButton.new(@main, 'command' => proc { term(@conn) } ) {
text 'Exit'
grid('row' => (row += 1), 'column' => 0, 'columnspan' => 2,
'sticky' => 'news')
}
# CR same as pushing login.
@main.bind('Return', proc { self.do_login(3) })
end end
# This is a window containing the file listing.
class FileWindow < TkFrame
def initialize(main)
super
# Set up the title appearance.
titfont = 'arial 16 bold'
titcolor = '#228800'
@conn = nil
# Label at top.
TkLabel.new(self) {
text 'FTP Download Agent'
justify 'center'
font titfont
foreground titcolor
pack('side' => 'top', 'fill' => 'x')
}
# Status label.
@statuslab = TkLabel.new(self) {
text 'Not Logged In'
justify 'center'
pack('side' => 'top', 'fill' => 'x')
}
# Exit button
TkButton.new(self) {
text 'Exit'
command { term(@conn) }
pack('side'=> 'bottom', 'fill' => 'x')
}
# List area with scroll bar.
The list area is disabled since we
# don't want the user to type into it.
@listarea = TkText.new(self) {
height 10
width 40
cursor 'sb_left_arrow'
state 'disabled'
pack('side' => 'left')
yscrollbar(TkScrollbar.new).pack('side' => 'right', 'fill' => 'y')
}
# Bind the system exit button to our exit.
main.protocol('WM_DELETE_WINDOW', proc { term(@conn) } )
# Create the login window.
LoginWindow.new(main, self, titfont, titcolor)
end
# Change the color of a tag for entering and leaving.
Unfortunately, there
# is no active color for tags in a text box.
def recolor(tag, color)
@listarea.tag_configure(tag, 'foreground' => color)
end
# Do a CD and load the contents.
If there is no directory name, skip
# the CD.
def load_dir(dir)
if dir
begin
@conn.chdir(dir)
rescue
thud('No ' + dir, $!)
end
@statuslab.configure('text' => "[Loading " + dir + "]")
else
@statuslab.configure('text' => '[Loading Home Dir]')
end
update
# Get the list of files.
files = [ ]
dirs = [ ]
sawdots = false
@conn.list() do |line|
# Real lines start with the perm bits.
And we don't want specials.
if line =~ /^[\-d]([r\-][w\-][x\-]){3}/
# Extract the useful parts, toss the bones.
The limit keeps us from
# dividing file names containing spaces.
parts = line.split(/\s+/, 9)
if parts.length >= 9
fn = parts.pop()
sawdots = true if fn == '..'
if parts[0][0..0] == 'd'
dirs.push(fn)
else
files.push(fn)
end
end
end
end
# Add .. if not present, then sort the list.
dirs.push('..') unless sawdots
files.sort!
dirs.sort!
# Clear the old contents from the directory listing box.
@listarea.configure('state' => 'normal')
@listarea.delete('1.0', 'end')
# Fill in the directories.
Bind for directory load (us).
ct = 0
while fn = dirs.shift
tagname = "fn" + ct.to_s
@listarea.insert('end', fn+"\n", tagname)
@listarea.tag_configure(tagname, 'foreground' => '#4444FF')
@listarea.tag_bind(tagname, 'Button-1',
proc { |f| self.load_dir(f) }, fn)
@listarea.tag_bind(tagname, 'Enter',
proc { |t| self.recolor(t, '#0000aa') },
tagname)
@listarea.tag_bind(tagname, 'Leave',
proc { |t| self.recolor(t, '#4444ff') },
tagname)
ct += 1
end
# Fill in the files. Bind for download.
while fn = files.shift
tagname = "fn" + ct.to_s
@listarea.insert('end', fn+"\n", tagname)
@listarea.tag_configure(tagname, 'foreground' => 'red')
@listarea.tag_bind(tagname, 'Button-1',
proc { |f| self.dld_file(f) }, fn)
@listarea.tag_bind(tagname, 'Enter',
proc { |t| self.recolor(t, '#880000') },
tagname)
@listarea.tag_bind(tagname, 'Leave',
proc { |t| self.recolor(t, 'red') },
tagname)
ct += 1
end
# Lock it up so the user can't mess with it.
@listarea.configure('state' => 'disabled')
# Update the status label.
begin
loc = @conn.pwd()
rescue
thud('PWD Failed', $!)
loc = '???'
end
@statuslab.configure('text' => loc)
end
# Download the file.
def dld_file(fn)
# Announce.
@statuslab.configure('text' => "[Retrieving " + fn + "]")
update
# Get the file.
begin
@conn.getbinaryfile(fn)
rescue
thud('DLD Failed', fn + ': ' + $!)
@statuslab.configure('text' => '')
else
@statuslab.configure('text' => 'Got ' + fn)
end
end
# This is a hook that the login window calls after a successful login.
# The login window makes the connection and attempts to login.
When this
# succeeds, it calls setconn() and destroys itself.
Setconn records the
# connection (which the login box created), then does the initial
# directory load.
def setconn(conn)
@conn = conn
load_dir(nil)
end end
# Create the main window, set the default colors, create the GUI, then
# fire the sucker up.
BG = '#E6E6FA'
root = TkRoot.new('background' => BG) { title "FTP Download" }
TkOption.add("*background", BG)
TkOption.add("*activebackground", '#FFE6FA')
TkOption.add("*foreground", '#0000FF')
TkOption.add("*activeforeground", '#0000FF')
FileWindow.new(root).pack()
Tk.mainloop
Building a 30 line Ruby HTTP server
How HTTP and TCP work together
TCP is a transport protocol that describes how a server and a client exchange data.
HTTP is a request-response protocol that specifically describes how web servers exchange data with HTTP clients or web browsers.
HTTP commonly uses TCP as its transport protocol.
In essence, an HTTP server is a TCP server that "speaks" HTTP.
# tcp_server.rb
require 'socket'
server = TCPServer.new 5678
while session = server.accept
session.puts "Hello world! The time is #{Time.now}"
session.close
end
In this example of a TCP server, the server binds to port 5678 and waits for a client to connect.
When that happens, it sends a message to the client, and then closes the connection.
After it's done talking to the first client, the server waits for another client to connect to send its message to again.
# tcp_client.rb
require 'socket'
server = TCPSocket.new 'localhost', 5678
while line = server.gets
puts line
end
server.close
To connect to our server, we'll need a TCP client.
This example client connects to the same port (5678) and uses server.gets to receive data from the server, which is then printed.
When it stops receiving data, it closes the connection to the server and the program will exit.
When you start the server server is running ($ ruby tcp_server.rb), you can start the client in a separate tab to receive the server's message.
shell
$ ruby tcp_client.rb
Hello world! The time is 2016-11-23 15:17:11 +0100
$
With a bit of imagination, our TCP server and client work somewhat like a web server and a browser.
The client sends a request, the server responds, and the connection is closed.
That's how the request-response pattern works, which is exactly what we need to build an HTTP server.
Before we get to the good part, let's look at what HTTP requests and responses look like.
A basic HTTP GET request
The most basic HTTP GET request is a request-line without any additional headers or a request body.
shell
GET / HTTP/1.1\r\n
The Request-Line consists of four parts:
A method token (GET, in this example)
The Request-URI (/)
The protocol version (HTTP/1.1)
A CRLF (a carriage return: \r, followed by line feed: \n) to indicate the end of the line
The server will respond with an HTTP response, which may look like this:
shell
HTTP/1.1 200\r\nContent-Type: text/html\r\n\r\n\Hello world!
This response consists of:
A status line: the protocol version ("HTTP/1.1"), followed by a space, the response's status code ("200"), and terminated with a CRLF (\r\n)
Optional header lines.
In this case, there's only one header line ("Content-Type: text/html"), but there could be multiple (separated with with a CRLF: \r\n)
A newline (or a double CRLF) to separate the status line and header from the body: (\r\n\r\n)
The body: "Hello world!"
A Minimal Ruby HTTP server
Enough talk.
Now that we know how to create a TCP server in Ruby and what some HTTP requests and responses look like, we can build a minimal HTTP server.
You'll notice that the web server looks mostly the same as the TCP server we discussed earlier.
The general idea is the same, we're just using the HTTP protocol to format our message.
Also, because we'll use a browser to send requests and parse responses, we won't have to implement a client this time.
# http_server.rb
require 'socket'
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
session.print "HTTP/1.1 200\r\n" # 1
session.print "Content-Type: text/html\r\n" # 2
session.print "\r\n" # 3
session.print "Hello world! The time is #{Time.now}" #4
session.close
end
After the server receives a request, like before, it uses session.print to send a message back to the client: Instead of just our message, it prefixes the response with a status line, a header and a newline:
The status line (HTTP 1.1 200\r\n) to tell the browser that the HTTP version is 1.1 and the response code is "200"
A header to indicate that the response has a text/html content type (Content-Type: text/html\r\n)
The newline (\r\n)
The body: "Hello world! …"
Like before, it closes the connection after sending the message.
We're not reading the request yet, so it just prints it to the console for now.
If you start the server and open http://localhost:5678 in your browser, you should see the "Hello world! …"-line with the current time, like we received from our TCP client earlier.
Serving a Rack app
Until now, our server has been returning a single response for each request.
To make it a little more useful, we could add more responses to our server.
Instead of adding these to the server directly, we'll use a Rack app.
Our server will parse HTTP requests and pass them to the Rack app, which will then return a response for the server to send back to the client.
Rack is an interface between web servers that support Ruby and most Ruby web frameworks like Rails and Sinatra.
In its simplest form, a Rack app is an object that responds to call and returns a "tiplet", an array with three items: an HTTP response code, a hash of HTTP headers and a body.
app = Proc.new do |env|
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
In this example, the response code is "200", we're passing "text/html" as the content type through the headers, and the body is an array with a string.
To allow our server to serve responses from this app, we'll need to turn the returned triplet into a HTTP response string.
Instead of always returning a static response, like we did before, we'll now have to build the response from the triplet returned by the Rack app.
# http_server.rb
require 'socket'
app = Proc.new do
['200', {'Content-Type' => 'text/html'}, ["Hello world! The time is #{Time.now}"]]
end
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
# 1
status, headers, body = app.call({})
# 2
session.print "HTTP/1.1 #{status}\r\n"
# 3
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
# 4
session.print "\r\n"
# 5
body.each do |part|
session.print part
end
session.close
end
To serve the response we've received from the Rack app, there's some changes we'll make to our server:
Get the status code, headers, and body from the triplet returned by app.call.
Use the status code to build the status line
Loop over the headers and add a header line for each key-value pair in the hash
Print a newline to separate the status line and headers from the body
Loop over the body and print each part.
Since there's only one part in our body array, it'll simply print our "Hello world"-message to the session before closing it.
Reading requests
Until now, our server has been ignoring the request variable.
We didn't need to as our Rack app always returned the same response.
Rack::Lobster is an example app that ships with Rack and uses request URL parameters in order to function.
Instead of the Proc we used as an app before, we'll use that as our testing app from now on.
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
# ...
Opening the browser will now show a lobster instead of the boring string it printed before.
Lobstericious!
The "flip!" and "crash!" links link to /?flip=left and /?flip=crash respectively.
However, when following the links, the lobster doesn't flip and nothing crashes just yet.
That's because our server doesn't handle query strings right now.
Remember the request variable we ignored before? If we look at our server's logs, we'll see the request strings for each of the pages.
shell
GET / HTTP/1.1
GET /?flip=left HTTP/1.1
GET /?flip=crash HTTP/1.1
The HTTP request strings include the request method ("GET"), the request path (/, /?flip=left and /?flip=crash), and the HTTP version.
We can use this information to determine what we need to serve.
# http_server.rb
require 'socket'
require 'rack'
require 'rack/lobster'
app = Rack::Lobster.new
server = TCPServer.new 5678
while session = server.accept
request = session.gets
puts request
# 1
method, full_path = request.split(' ')
# 2
path, query = full_path.split('?')
# 3
status, headers, body = app.call({
'REQUEST_METHOD' => method,
'PATH_INFO' => path,
'QUERY_STRING' => query
})
session.print "HTTP/1.1 #{status}\r\n"
headers.each do |key, value|
session.print "#{key}: #{value}\r\n"
end
session.print "\r\n"
body.each do |part|
session.print part
end
session.close
end
To parse the request and send the request parameters to the Rack app, we'll split the request string up and send it to the Rack app:
Split the request string into a method and a full path
Split the full path into a path and a query
Pass those to our app in a Rack environment hash.
For example, a request like GET /?flip=left HTTP/1.1\r\n will be passed to the app like this:
{
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/',
'QUERY_STRING' => '?flip=left'
}
Restarting our server, visiting http://localhost:5678, and clicking the "flip!"-link will now flip the lobster, and clicking the "crash!" link will crash our web server.
We've just scratched the surface of implementing a HTTP server, and ours is only 30 lines of code, but it explains the basic idea.
It accepts GET requests, passes the request's attributes to a Rack app, and sends back responses to the browser.
Although it doesn't handle things like request streaming and POST requests, our server could theoretically be used to serve other Rack apps too.