Monday, January 30, 2012

Exercise 48: Advanced User Input: Learn Ruby the Hard Way: Practicum

So it's been awhile since I've posted one of these practicum updates. I could say there's a lot of reasons why. I got sidetracked into doing a tester's autobiography, or that my work and family life accelerated into areas that I needed to get taken acare of and this had to take a back burner. I could say all of those things, but they'd be excuses. No, the reason why I haven't posted in awhile is that, well, I'm stuck.

this is the first assignment I have not been able to get to work, and I think it's because of what it requires us to do. zed has given us a test, and we are to write the code to make the tests pass. So far, I've not been able to do it. that probably says a whole lot more about me than it does Zed, but be that as it may, I cannot call this project complete or objective if I only post the things I can do well. I said I'd also post the areas I was struggling with, and well, here they are:

The point to this assignment is that, right now, if we want to use our game and answer questions in the game, we have to be exact in our terminology. "Open the Door" is OK, but:

- open door
- go THROUGH the door

will cause an error or not give us the right response, even though they are saying the same thing.

the goal is that we want to write some classes that will work as a library of possible responses, and in this regard, we want to do the following:


- We have Words separated by spaces.
- We have Sentences composed of the words.
- We have Grammar that structures the sentences into meaning.

Our Game Lexicon

In our game we have to create a Lexicon of words:

Direction words: north, south, east, west, down, up, left, right, back.
Verbs: go, stop, kill, eat.
Stop words: the, in, of, from, at, it
Nouns: door, bear, princess, cabinet.
Numbers: any string of 0 through 9 characters.


Breaking Up A Sentence

The process to break up a sentence into individual words is fairly simple, and looks like this:

stuff = gets.chomp()
words = stuff.split()

Below is what Zed and Rob provide to help us understand what is happening. On the surface, I get what they are saying, but getting my left hand to get what the right hand is doing, I'm still working through (note, if I figure this out, or someone helps me through this, I will post the solution provided and credit to the person who helps me figure it out :) ).

Lexicon Structs

Once we know how to break up a sentence into words, we just have to go through the list of words and figure out what "type" they are. To do that we're going to use a handy little Ruby structure called a "struct". A struct is a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class. It's created like this:

Pair = Struct.new(:token, :word)
first_word = Pair.new("direction", "north")
second_word = Pair.new("verb", "go")
sentence = [first_word, second_word]

This creates a pair of (TOKEN, WORD) that lets you look at the word and do things with it.


Scanning Input

Now you are ready to write your scanner. This scanner will take a string of input from a user and return a sentence that's composed of a list of structs with the (TOKEN, WORD) pairings. If a word isn't part of the lexicon then it should still return the WORD, but set the TOKEN to an error token. These error tokens will tell the user they messed up.


Here's where it gets fun. I'm not going to tell you how to do this. Instead I'm going to write a unit test und you are going to write the scanner so that the unit test works.

Exceptions And Numbers

An area that Zed has provided for us to consider when it comes to converting numbers:


There is one tiny thing I will help you with first, and that's converting numbers. In order to do this though, we're going to cheat and use exceptions. An exception is an error that you get from some function you may have run. What happens is your function "raises" an exception when it encounters an error, then you have to handle that exception. For example, if you type this into IRB:

ruby-1.9.2-p180 :001 > Integer("hell")
ArgumentError: invalid value for Integer(): "hell"
    from (irb):1:in `Integer'
    from (irb):1
    from /home/rob/.rvm/rubies/ruby-1.9.2-p180/bin/irb:16:in `
'


That ArgumentError is an exception that the Integer() function threw because what you handed Integer() is not a number. The Integer() function could have returned a value to tell you it had an error, but since it only returns numbers, it'd have a hard time doing that. It can't return -1 since that's a number. Instead of trying to figure out what to return when there's an error, the Integer() function raises the TypeError exception and you deal with it.


You deal with an exception by using the begin and rescue keywords:

def convert_number(s)
  begin
    Integer(s)
  rescue ArgumentError
    nil
  end
end

You put the code you want to "begin" inside the begin block, and then you put the code to run for the error inside the rescue. In this case, we want to call Integer() on something that might be a number. If that has an error, then we "rescue" it and return nil instead.


In your scanner that you write, you should use this function to test if something is a number. You should also do it as the last thing you check for before declaring that word an error word.

What You Should Test

Here are the files test/test_lexicon.rb that I am working with:



Design Hints

Focus on getting one test working at a time. Keep this simple and just put all the words in your lexicon in lists that are in your lexicon.rb file. Do not modify the input list of words, but instead make your own new list with your lexicon pairs in it. Also, use the include? method with these lexicon arrays to check if a word is in the lexicon.

Extra Credit

- Improve the unit test to make sure you cover more of the lexicon.
- Add to the lexicon and then update the unit test.
- Make your scanner handles user input in any capitalization and case.
- Update the test to make sure this actually works.
- Find another way to convert the number.
- My solution was 37 lines long. Is yours longer? Shorter?

TESTHEAD's TAKEAWAY:

Test Driven Development is not as easy as it seems, at least as I currently understand it. Again, I'm not blaming Zed or Rob here, I'm blaming myself for not quite geting how to do this. this and the next ercise are by necessity going to need to be considered "open ended" for the time being until I can grok what I need to do to make this work. Sometimes that takes beating against it, sometimes it means doing something else until I can see the connection. Either way, my goal was to review and complete this by the end of January, and I'm running low on time now, so I need to get a move on and come backto this one later.

2 comments:

Emily said...

Hi Michael,
I did this exercise I few days ago. My solution used hashes and it took a lot of tweaking to get all the tests to pass.
I know I didn't do it the way Zed/Rob meant because they wouldn't have made it so inelegant. Still I can show you my code if it would help.
Emily
@emmypemmy

Michael Larsen said...

Hi Emily, I'd certainly be interested in seeing your solution and trying it against the one's I've played with thus far.

Thanks,
--MKL
@mkltesthead