CS 240 Homework 2 - Ruby
Due Fri Sep 19 Sun Sep 21 at 11:59pm
Overview
In this assignment you will do some simple programming exercises to get familiar and comfortable with the Ruby language. You will also practice using the testing tool RSpec
to automatically test and grade your code.
Objectives
- Practice writing Ruby code, methods, and classes.
- Practice running
RSpec
tests on your code
Necessary Files
You will want to get the starter code and instructions from the GitHub homework
repository. Be sure and pull the latest version of the homework upstream
repo, per the instructions in
Hwk1.
The starter code you're looking for is in the hwk2
folder.
Assignment Details
If you have time, it may be helpful to go through a tutorial on using Ruby. I recommend either Ruby in Twenty Minutes or the Codecademy Ruby course.
The goal of this multi-part assignment is to get you accustomed to basic Ruby coding and introduce you to RSpec, the unit testing tool we will be using heavily.
RSpec
RSpec is a tool testing Ruby code (see Sections 8.1 and 8.2 of the text). It lets you define a series of tests (a "spec") which can be run to tell you if your code does what it is supposed to. While I will provide explanations of what your code should do, you should get accustomed to the idea that with Test-Driven Development, the true specification for code is in these test files!
I have provided test files for all of the code you need to write in this assignment: you can thus check your work by running the appopriate spec file.
-
To run the tests, in the termina change directory to the root directory of this assignment (
hwk2
, which should contain subdirectorieslib/
andspec/
). You can then test a particular code file by runningrspec spec/<file_name>_spec.rb
wherefile_name
is the name of the file you want to test (each file comes with the appropriate _spec.rb file). So for example, you could testwarmup.rb
with the commandrspec spec/warmup_spec.rb
Note that RSpec expects to find code files underlib/
and the corresponding spec files underspec/
.- You need to have rspec 2.14 installed for these spec files to work properly.
-
Initially, are the tests are marked as "pending" (that is, they shouldn't be run repeatedly because there isn't code yet!), as indicated by the argument
:pending => true
in eachdescribe
block. To start testing a question, remove this option: e.g., inwarmup_spec.rb
, change line 7:describe "#hello", :pending => true do
describe "#hello" do
-
You can use autotest to automate the process of running these spec files. Simply run the command
autotest
from the command line (in the top level of thehwk2
directory), and it will re-run all the RSpec tests each time you make changes to your code. - Note that all the spec files contains a number of test cases. At a minimum, all should pass when you submit your code. I may run additional cases as well.
Appetizer: Warmup
Specs: spec/warmup_spec.rb
In the first part of the assignment you'll write some simple Ruby methods. Check the documentation for Array, Hash, Enumerable, and String, as they could help tremendously with these exercises.
- Define a method
hello(name)
that takes a string representing a name and returns the string "Hello, " concatenated with the name. - Define a method
sum
which takes an array of integers as an argument and returns the sum of its elements. For an empty array it should return zero. - Define a method
max_2_sum
which takes an array of integers as an argument and returns the sum of its two largest elements. For an empty array it should return zero. For an array with just one element, it should return that element. - Define a method
sum_to_n?
which takes an array of integers and an additional integer, n, as arguments and returns true if any two distinct elements in the array of integers sum to n. An empty array or single element array should both return false.
Entree: Fun With Strings
Specs: spec/fun_with_strings_spec.rb
In the second part of the assignment, you'll implement some functions that perform basic string processing. You should start from the provided fun_with_strings.rb
template. This arranges to mix your methods into the String class, so that methods can be called like
"redivider".palindrome? # => should return true "adam".palindrome? # => should return false or nil
-
Part A - Palindromes
A palindrome is a word or phrase that reads the same forwards as backwards, ignoring case, punctuation, and nonword characters. (A "nonword character" is defined for our purposes as "a character that Ruby regular expressions would treat as a nonword character".)
Write a method
palindrome?
that returns true if and only if its receiver is a palindrome.Your solution shouldn't use loops or iteration of any kind. Instead, you will find regular-expression syntax very useful; it's reviewed briefly in the textbook, and the website rubular.com lets you try out Ruby regular expressions "live". Some methods that you might find useful (which you'll have to look up in Ruby documentation at ruby-doc.org) include:
String#downcase
,String#gsub
, andString#reverse
. -
Part B - Word Count
Define a method
count_words
that, given an input string, return a hash whose keys are words in the string and whose values are the number of times each word appears:"To be or not to be".count_words # => {"to"=>2, "be"=>2, "or"=>1, "not"=>1}
Your solution shouldn't use for-loops, but iterators like
each
are permitted. As before, nonwords and case should be ignored. A word is defined as a string of characters between word boundaries. -
Part C - Anagrams
An anagram group is a group of words such that any one can be converted into any other just by rearranging the letters. For example, "rats", "tars" and "star" are an anagram group.
Given a String that is a space separated list of words, write a method that groups them into anagram groups and returns the array of groups. Case doesn't matter in classifying string as anagrams (but case should be preserved in the output), and the order of the anagrams in the groups doesn't matter.
Dessert: Basic Object-Oriented Programming
Specs: spec/dessert_spec.rb
In the third part of this assignment, you'll develop a simple class and do some basic object-oriented programming.
-
Create a class
Dessert
with getters and setters for name and calories. The constructor should accept arguments for name and calories. -
Define instance methods
healthy?
, which returns true if and only if a dessert has less than 200 calories, anddelicious?
, which returns true for all desserts. -
Create a class
JellyBean
that inherits fromDessert
. The constructor should accept a single argument giving the jelly bean's flavor; a newly-created jelly bean should have 5 calories and its name should be the flavor plus "jelly bean", for example, "strawberry jelly bean". - Add a getter and setter for the flavor.
- Modify
delicious?
to return false if the flavor islicorice
, but true for all other flavors. The behavior ofdelicious?
for non-jelly-bean desserts should be unchanged.
Coffee: Ruby Metaprogramming
Specs: spec/attr_accessor_with_history_spec.rb
In the last part of the assignment you will perform some in-depth metaprogramming to extend Ruby classes. Note that this part is conceptually challenging but not programmatically complex.
The textbook discusses how attr_accessor
uses metaprogramming to create getters and setters for object attributes on the fly. Define a method attr_accessor_with_history
that provides the same functionality as attr_accessor
but also tracks every value the attribute has ever had:
class Foo attr_accessor_with_history :bar end f = Foo.new f.bar = 3 # => 3 f.bar = :wowzo # => :wowzo f.bar = 'boo!' # => 'boo!' f.bar_history # => [nil, 3, :wowzo]
Calling bar_history
before bar
's setter is ever called should return nil
.
History of instance variables should be maintained separately for each object instance. that is:
f = Foo.new f.bar = 1 ; f.bar = 2 g = Foo.new g.bar = 3 ; g.bar = 4 g.bar_history
then the last line should just return [nil,3]
, rather than [nil,1,3]
.
If you're interested in how the template works, the first thing to notice is that if we define attr_accessor_with_history
in class Class
, we can use it as in the snippet above. This is because a Ruby class like Foo
or String
is actually just an object of class Class
. (If that makes your brain hurt, just don't worry about it for now. It'll come.)
The second thing to notice is that Ruby provides a method class_eval
that takes a string and evaluates it in the context of the current class, that is, the class from which you're calling attr_accessor_with_history
. This string will need to contain a method definition that implements a setter-with-history for the desired attribute attr_name
. In short: this method will contain inside it the code to define an accessor for the arbitrary variable with attr_name
, where the name of the variable is itself a variable. Wow. Much meta.
If you think of Generics in Java, where the "type" of the generic is itself a variable, you can get a feel for what is going on here.
Hints:
-
Note that one powerful metaprogramming feature in Ruby is
class_eval
that can be called in the meta-class Class.class_eval
can interpret a string on the fly to create some new code. In the example below, we defineadd_method()
to the meta-class (and, through inheritance, to any class). When called, this method defines a new method that returns a string saying hello to some name (notice how#{name}
gets replaced with the parameter passed to add_method, so that callingadd_method(hello_joel)
causes class_eval to execute the codedef hello_joel() ...
).class Class def add_method(name) class_eval %Q{ def hello_#{name}() 'hello #{name}' end } end end class MyClass add_method :hello_joel end mc = MyClass.new puts mc.hello_joel # => 'hello joel'
- Don't forget that the very first time the attribute receives a value, its history array will have to be initialized.
- An attribute's initial value is always
nil
by default, so iffoo_history
is referenced beforefoo
has ever been assigned, the correct answer isnil
(no history), but after the first assignment tofoo
, the correct value forfoo_history
would be[nil]
(it has history, but that history was nothing). - Don't forget that instance variables are referred to as
@bar
within getters and setters, as Section 3.4 of the ESaaS textbook explains. - Although the existing
attr_accessor
can handle multiple arguments (e.g.attr_accessor :foo, :bar
), your version just needs to handle a single argument. - Your implementation should be general enough to work in the context of any class and for attributes of any (legal) variable name. This is easier than it may seem, and comes for free with good metaprogramming practice.
Submitting
Make sure that your code passes all tests, and then push your code out to your personal homework repo (which you set you in Homework 1).
Grading
Each part of this assignment will be graded out of 10 points, based on the number of tests that pass.