RubyCocoa Example: SpeakLine

I’m working my way through Cocoa Programming for Mac OS X by Aaron Hillegass, which would be the perfect book for what I want to learn, except it’s using Objective-C and not RubyCocoa. So I’m getting double the exercise, following along with the Obj-C examples and then translating into RubyCocoa. Today: Chapter 4. The example: a program that will speak a line the user types in.

I won’t go through the Interface Builder set-up, since it’s in the book, but the app will look like this:

SpeakLine window

If you’ve been playing around with Interface Builder, you should see from my code below what needs to be set up. (oh, and that thing in the bottom left corner is a Color Well)

Make a Cocoa-Ruby Application in XCode. Set up the nib file as it shows in the book, with one exception: don’t “Create Files for AppController” in Interface Builder, do that later by hand in XCode by adding a new “Ruby NSObject subclass” file to the project. Call it AppController.rb.

# the first line of any RubyCocoa file:
require 'osx/cocoa'

# create the class as a subclass of NSObject
class AppController < OSX::NSObject

# list the outlets that were created in Interface Builder,
# they'll be used as variables like @textField

ib_outlets :textField, :colorWell

# we're going to define an init method, but it's not
# necessary for all applications

def init
# when calling a super's method, preceed the method name by super_
super_init
# we'll throw a lot of stuff in the log for now,
# it'll show up when the program is run

OSX::NSLog("init")
# creating a new variable of type NSSpeechSynthesizer
# with the default system voice

@speechSynth = OSX::NSSpeechSynthesizer.alloc.init
# when overriding the init command, make sure to return self:
self
end

# awakeFromNib is run after everything's initialized, but
# before the user can do anything. I'm not actually sure
# why both methods are necessary...

def awakeFromNib
# setting the color shown in the Color Well to be that of
# the initial contents of textField

@colorWell.setColor(@textField.textColor)
end

# the method to speak the given text
def sayIt(sender)
OSX::NSLog("sayIt")
# get string that the user entered
string = @textField.stringValue.to_s
# if it's empty, don't do anything
return if string.length == 0
# speak the text
@speechSynth.startSpeakingString(string)
end

# stop speaking the given text
def stopIt(sender)
# here I tried puts intead of NSLog, just to see what the difference was
puts("stopIt")
# stop speaking. simple, yes?
@speechSynth.stopSpeaking
end

# change the color of the text in the textField based on
# what the user chooses from the Color Well

def changeTextColor(sender)
@textField.setTextColor(sender.color)
OSX::NSLog("changing text color")
end

end

Build. Run. Make your computer say dirty things.

Ruby Example: toRoman

Problem: convert a number to its equivalent in Roman numerals.

Solution shows use of cases, array and string manipulation, exception raising, and an “each” block.

Working through the problem in irb, we start with an array of the Roman numerals:

irb(main):001:0> roman_array = ['i', 'v', 'x', 'l', 'c', 'd', 'm']
=> ["i", "v", "x", "l", "c", "d", "m"]

and a given number, num. For example, set

irb(main):002:0> num = 2683
=> 2683

Next, we turn this into an array of characters:

irb(main):003:0> char_array = num.to_s.split(//)
=> ["2", "6", "8", "3"]

Because we want to convert the ones column first, then tens etc. until we hit the end of the number, it’s convenient to reverse the array (instead of traversing it backwards):

irb(main):004:0> char_array.reverse!
=> ["3", "8", "6", "2"]

Here I’m going to throw in a method called trans that translates a single digit into roman numerals. I pass it a digit and an array with the Roman numerals associated with the correct power of ten (the one, five, and ten). For example, to translate the “3″ in the number “37″, I call trans(3, ["x", "l", "c"]). Here’s the method:

irb(main):005:0> def trans(passed_num, arr)
irb(main):006:1> case passed_num
irb(main):007:2> when 1 then arr[0]
irb(main):008:2> when 2 then arr[0]*2
irb(main):009:2> when 3 then arr[0]*3
irb(main):010:2> when 4 then arr[0]+arr[1]
irb(main):011:2> when 5 then arr[1]
irb(main):012:2> when 6 then arr[1]+arr[0]
irb(main):013:2> when 7 then arr[1]+arr[0]*2
irb(main):014:2> when 8 then arr[1]+arr[0]*3
irb(main):015:2> when 9 then arr[0]+arr[2]
irb(main):016:2> else ''
irb(main):017:2> end
irb(main):018:1> end
=> nil

Okay, hoping that makes sense, lets get back to our char_array. For an element of char_array we can find the correct subarray of roman_array to send to trans because we know that the form of char_array is now [ones, tens, hundreds, thousands]. For the digit at char_array[0] we want to send the subarray roman_array[0,3]. For char_array[1] we want roman_array[2,3]. And so on. So for char_array[i], we want to pass the subarray roman_array[i*2, 3].The call, then, to change an element of char_array to its Roman counterpart, is

char_array[i] = trans(char_array[i].to_i, roman_array[i*2, 3])

(note: I’m overwriting the array instead of creating a new array with the Roman numerals, for no real reason at all).We want to do this for each element of char_array, which looks like this:

irb(main):019:0> char_array.each_index{|i| char_array[i] = trans(char_array[i].to_i, roman_array[i*2, 3])}
=> ["iii", "lxxx", "dc", "mm"]

We then want to reverse the order again, and stick the pieces back together:

>irb(main):020:0> char_array.reverse.to_s
=> "mmdclxxxiii"

which gives us our number, converted into Roman numerals.

Putting it all together (apologies for the lack of indentation):

def toRoman(num)
raise "this program only handles 1..3999" if num>=4000 or num<=0
roman_array = ['i', 'v', 'x', 'l', 'c', 'd', 'm']
char_array = num.to_s.split(//).reverse
char_array.each_index{|i| char_array[i] = trans(char_array[i].to_i, roman_array[i*2, 3])}.reverse.to_s
end
def trans(passed_num, arr)
case passed_num
when 1 then arr[0]
when 2 then arr[0]*2
when 3 then arr[0]*3
when 4 then arr[0]+arr[1]
when 5 then arr[1]
when 6 then arr[1]+arr[0]
when 7 then arr[1]+arr[0]*2
when 8 then arr[1]+arr[0]*3
when 9 then arr[0]+arr[2]
else ''
end
end

To use these methods as a stand-alone, useful .rb file, make the first line in the file:

#!usr/bin/ruby

or whatever works for your path, then at the end of the file, after the two methods:

if __FILE__ == $0
toRoman(ARGV[0].to_i)
end

This lets you call the method toRoman from the terminal, or use it as a library file in other code. A terminal call would look like:

$ ruby toRoman.rb 3195
mmmcxcv

while to use the method in other ruby code, include the line

irb(main):002:0> require 'toRoman.rb'
=> true

and then call the method as

irb(main):003:0> toRoman(3195)
=> "mmmcxcv"

Review of RubyCocoa Tutorials

I’d been playing around with Ruby for a few weeks, making some little programs that I was happy with, when I decided it was time to venture into GUI. The obvious choice for my mac-loving self was Cocoa, so I found the RubyCocoa gem and went off in search for tutorials. I had never touched XCode, Cocoa, Objective-C, or InterfaceBuilder before. It’s a lot to learn all at once, but I found some sites that have helped.

This tutorial is the first one that I tried to work through. The instructions are very detailed, which I found extremely helpful as I’d never worked with XCode or InterfaceBuilder before. Being told exactly what to click on, along with tons of screenshots, was exactly what I needed in my first tutorial. By the end, I was left with a toy program that did work (there was one mis-named class in the tutorial, easy to spot if you’ve got your eyes open, or read the comments at the bottom). However, I didn’t feel like I had actually learned much. I knew how to make that program, but didn’t really understand much of what I did or have any idea how to extend it or make other programs. Detailed instructions good, explanations lacking.

Next up was an Introduction to RubyCocoa from rubycocoa.com. Not really a tutorial, but discussion of the parts of RubyCocoa (ruby, cocoa, objective-c) and where to find (mostly offline) resources for each. Elsewhere at the site is a guide to writing a small Asteroids-type game with RubyCocoa, but I haven’t had success with it. The instructions were too high-level, I didn’t understand XCode or InterfaceBuilder enough to follow them. I’ve gone back to this tutorial after I’d learned a bit more, and now can’t get the ruby code to work. I don’t know what I’m missing, but thumbs way, way down on this one.

After that frustration, I decided it was time for a purely Cocoa tutorial, that would hopefully help me understand XCode and InterfaceBuilder enough to focus more on the ruby aspect of what I wanted to be doing. Cocoa Dev Central has two fantastic introductory tutorials, Learn Cocoa and Learn Cocoa II. Aimed at people who have never coded, they did a great job of explaining what was going on in XCode and InterfaceBuilder. I was starting to understand. This is the tutorial I should have started with.

Happy with my burgeoning understanding, I wanted to work through another tutorial or two to get more of a feel for the Objective-C aspect of RubyCocoa. I was thrilled to discover Your first few days on RubyCocoa at Meta | ateM. With the newcocoa gem I could use RubyCocoa without XCode! This tutorial was a snap (at this point I don’t know if it’s because it’s a really good tutorial, or because I’ve picked things up from the other tutorials), and I’m excited now about trying my own code. Brilliant. So glad I found this one.

Ambition

Things I’m trying to learn, all at the same time:

  • postgreSQL
  • ruby
  • XCode
  • cocoa
  • objective-c

My goal is to learn a bit more about coding (I took a couple of courses in Java back in first year uni, it was a while ago) by making a GTD app. It’s a lot to get my head around all at once, so welcome to my attempt at project-tracking.

Follow

Get every new post delivered to your Inbox.