6 September 2005
Going from Java to Ruby

One of the best parts of being a software developer is occasionally getting to change the way you think in a fundamental way. Besides learning about new domains or getting new development or infrastructure tools, every once in a while you become aware of a compelling new language that allows you to re-experience the joy of learning to write software. I’ve enjoyed the feeling four times in my life (I’ve learned many more languages than that, but not all were joyful experiences) and now my programming-consciousness is expanding as I learn Ruby.

If you haven’t heard much of Ruby, allow me to clue you in. Ruby is a dynamic object-oriented scripting language. It was created by Yukihiro “Matz” Matsumoto in Japan in 1993, and first released publicly in 1995. It escaped note in the West for a while primarily because there wasn’t a good translation of the documentation from Japanese. The popularity of Ruby has now started picking up as more information on the language began to appear - especially when pragmatic programmer Dave Thomas wrote about the language in his book known affectionately as The Pickaxe - Programming Ruby. Today, Ruby continues to gain momentum: the ascent of the language accelerating.

For the Java crowd, the Ruby ‘object-oriented’ concept is object-oriented in the extreme: everything is an object. Not just the domain entities, built-in classes or library widgets - I mean everything. At the high end of Ruby programming, you can write objects that write objects and use them right there; you can add and remove methods and mix-in modules at run-time; your variables don’t have static types and so can hold any object that you assign to them; you can use duck-typing or even call possibly non-existent methods on an object, catch the exception if it occurs and do something else; and you can even pass blocks of code as objects to methods. And at the low end - there is no low end: there are no primitives types in the language. It’s all just objects.

What really makes Ruby tick is the dynamic programming paradigm. Ruby isn’t compiled as Java is. In Ruby, you don’t know for sure if your code will run correctly until you actually run it. Well, you do, but only because you wrote and tested it - not just because a compiler told you your statically-typed program didn’t generate any compile errors. Some of you may immediately ask, “What if I make a horrible mistake?” This isn’t really a new problem - we all know even the best Java code can still crash, even though it compiles just fine. “So what should I do?” You should unit test everything! You do use unit-testing to find your errors, right? And you do create code agilely so that the horrible mistakes are found early, right? If not, it’s time to make these changes before worrying about the false security of type safety. Anyway, the good thing is that when you’re around Ruby for a while you stop thinking about types in the same way you have been - they just sort of go away.

Ruby is a language that focuses on convention over configuration. Instead of writing a lot of machinery into your code, in Ruby you get to piggyback on the shoulders of giants. By following conventions, using mix-ins, duck-typing and passing code blocks, your Ruby code can immediately work within existing frameworks. If you think in the Ruby way when you’re writing your code, others can use your objects seamlessly. Perhaps this is most evident in David Heinemeier Hanson’s Ruby on Rails. Rails will blow your mind if you’re used to doing web application development with J2EE, beans, struts or Microsoft’s .Net. By using the Rails framework and Ruby, you can create serious web applications in hours and days instead of weeks and months. Really. The Rails stuff just works, and much of this is due to the versatility of Ruby.

I am trying hard here to convince you that you need to at least look at Ruby. When I first heard about Ruby, I thought, “I don’t need a scripting language, I’m writing Java. I’m busy.” Then I took some time, looked and learned, and was enlightened. Ruby works. Ruby is cool. You need to learn it and make your software life better and more fun. It isn’t going to replace Java, but it is going to have a place in your systems. And it will put a smile on your face.

Although Ruby is free, learning Ruby requires a significant personal investment. Part of my path to Ruby was by way of re-coding some software I’ve been carrying around and evolving for the last thirty years, and I’m going to discuss one of those efforts here. I’d written much of this code as functions and objects with various levels of sophistication, generally in C, Objective-C, LISP, C++, Java and C#. I’ll focus on two of these objects, the Duration and the Stopwatch. For each I’ll start with the Java class and then work through the transition to Ruby. I won’t go deeply into the details of the Ruby language, as others have already done a good job of that, but instead I’ll concentrate on the change in perspective and the different feel of developing in Ruby.

Duration.java to duration.rb

The first class I’ll discuss is Duration, an object I’d brought into Java from my Objective-C and C++ code written ten or so years ago, and was originally formed from C functions I pulled together fifteen years before that. One good thing about long-lived code like this: it’s solid. In one form or another it’s been tempered through use for a very long time. I also chose Duration because I usually find fairly minimal treatment given to elapsed time in most standard libraries.

A duration measures elapsed time. It’s a quantity of time-passage that can stand alone (“I’ll be just a minute”), exist relative to a time (“two hours from now”) or be defined by a start and end time (“between ten and eleven-thirty”). A duration can be added, subtracted, broken into components and compared. But despite the fact that many of the real-life things we do every day have temporal components, a duration does not typically get first-class object consideration. In Java, nearly every instance of elapsed time I’ve run across is a simple rendering of the difference between two Date objects. I’ve found raising a duration to first class status is quite useful.

When I code an object with the potential for broad use, I spend a little bit more time on it. When I’m not sure how I’ll reuse something, but I’m pretty sure I will, I go the extra mile. The Java code I started from includes seven groups of methods for various purposes: constructing, getting, setting, accumulating, rendering, comparing and rounding. Before I go into the Ruby, lets take a look at the Java code.

// Duration.java

package com.eymiha.util;

import java.io.Serializable;
import java.util.Date;

public class Duration implements Comparable, Serializable {
  private static final long serialVersionUID = 1L;

  private static boolean useWeeks = true;

  private long duration;
  private boolean usesWeeks = false;

  public static final int type_millis = 1;
  public static final int type_seconds = 2;
  public static final int type_minutes = 3;
  public static final int type_hours = 4;
  public static final int type_days = 5;
  public static final int type_weeks = 6;

  public static void useWeeks (boolean willUseWeeks) {
    useWeeks = willUseWeeks;
  }

  public static boolean willUseWeeks () {
    return useWeeks;
  }

  public Duration (long value) {
    set(value);
    usesWeeks = useWeeks;
  }

  public Duration () {
    this(0);
  }

  public Duration (Duration duration) {
    this(duration.duration);
  }

  public void usesWeeks (boolean usesWeeks) {
    this.usesWeeks = usesWeeks;
  }

  public boolean usesWeeks () {
    return usesWeeks;
  }

  public void add (long value) {
    duration += value;
  }

  public void add (Duration duration) {
    add(duration.duration);
  }

  public void set (long value) {
    duration = value;
  }

  public void set (Duration duration) {
    set(duration.duration);
  }

  public long get () {
    return duration;
  }

  public String toString () {
    return toString(usesWeeks ? type_weeks : type_days);
  }

  public static long getMillisInType (int type) {
    long multiplier;
    switch (type) {
    case type_weeks:
      multiplier = 7 * 24 * 60 * 60 * 1000;
      break;
    case type_days:
      multiplier = 24 * 60 * 60 * 1000;
      break;
    case type_hours:
      multiplier = 60 * 60 * 1000;
      break;
    case type_minutes:
      multiplier = 60 * 1000;
      break;
    case type_seconds:
      multiplier = 1000;
      break;
    case type_millis:
      multiplier = 1;
      break;
    default:
      multiplier = 0;
      break;
    }
    return multiplier;
  }

  public void add (int type, long value) {
    duration += value * getMillisInType(type);
  }

  public void set (int type, long value) {
    duration = value * getMillisInType(type);
  }

  public int getPart (int type) {
    long multiplier = getMillisInType(type);
    switch (type) {
    case type_weeks:
      return (int) ((duration / multiplier));
    case type_days:
      return (usesWeeks) ? ((int) ((duration / multiplier) % 7))
          : ((int) (duration / multiplier));
    case type_hours:
      return (int) ((duration / multiplier) % 24);
    case type_minutes:
      return (int) ((duration / multiplier) % 60);
    case type_seconds:
      return (int) ((duration / multiplier) % 60);
    case type_millis:
      return (int) ((duration / multiplier) % 1000);
    default:
      return 0;
    }
  }

  public long getIn (int type) {
    return duration / getMillisInType(type);
  }

  public String toString (int type) {
    long value = duration;
    String hours, minutes, seconds, millis;
    StringBuffer buffer = new StringBuffer();
    if (value < 0) {
      buffer.append("-");
      duration = -duration;
    }
    switch (type) {
    case type_weeks:
      buffer.append(getIn(type_weeks)).append(":");
      boolean tempUsesWeeks = usesWeeks;
      usesWeeks = true;
      buffer.append(getPart(type_days)).append(":");
      usesWeeks = tempUsesWeeks;
      hours = "0" + getPart(type_hours);
      buffer.append(hours.substring(hours.length() - 2)).append(":");
      minutes = "0" + getPart(type_minutes);
      buffer.append(minutes.substring(minutes.length() - 2)).append(":");
      seconds = "0" + getPart(type_seconds);
      buffer.append(seconds.substring(seconds.length() - 2)).append(".");
      millis = "00" + getPart(type_millis);
      buffer.append(millis.substring(millis.length() - 3));
      break;
    case type_days:
      buffer.append(getIn(type_days)).append(":");
      hours = "0" + getPart(type_hours);
      buffer.append(hours.substring(hours.length() - 2)).append(":");
      minutes = "0" + getPart(type_minutes);
      buffer.append(minutes.substring(minutes.length() - 2)).append(":");
      seconds = "0" + getPart(type_seconds);
      buffer.append(seconds.substring(seconds.length() - 2)).append(".");
      millis = "00" + getPart(type_millis);
      buffer.append(millis.substring(millis.length() - 3));
      break;
    case type_hours:
      buffer.append(getIn(type_hours)).append(":");
      minutes = "0" + getPart(type_minutes);
      buffer.append(minutes.substring(minutes.length() - 2)).append(":");
      seconds = "0" + getPart(type_seconds);
      buffer.append(seconds.substring(seconds.length() - 2)).append(".");
      millis = "00" + getPart(type_millis);
      buffer.append(millis.substring(millis.length() - 3));
      break;
    case type_minutes:
      buffer.append(getIn(type_minutes)).append(":");
      seconds = "0" + getPart(type_seconds);
      buffer.append(seconds.substring(seconds.length() - 2)).append(".");
      millis = "00" + getPart(type_millis);
      buffer.append(millis.substring(millis.length() - 3));
      break;
    case type_seconds:
      buffer.append(getIn(type_seconds)).append(":");
      millis = "00" + getPart(type_millis);
      buffer.append(millis.substring(millis.length() - 3));
      break;
    case type_millis:
      buffer.append(getIn(type_millis));
      break;
    default:
      buffer.append("");
      break;
    }
    duration = value;
    return buffer.toString();
  }

  public static Duration between (long millis0, long millis1) {
    return new Duration(millis1 - millis0);
  }

  public static Duration between (Date date0, Date date1) {
    return between(date0.getTime(), date1.getTime());
  }

  public Duration roundTo (int type) {
    return roundTo(type, 1);
  }

  public Duration roundTo (int type, double fraction) {
    int sign = (duration < 0)? -1 : (duration > 0)? 1 : 0;
    long tempDuration = sign * duration;
    double multiplier = getMillisInType(type) * fraction;
    if (multiplier < 0)
      multiplier = -multiplier;
    if (multiplier < 1)
      multiplier = 1;
    return new Duration(
        sign*(((int)((tempDuration+(multiplier/2))/multiplier)) * (int)multiplier));
  }


  public int compareTo (Object object) {
    return AuxMath.sign(duration - ((Duration) object).duration);
  }

  public boolean equals (Object object) {
    return (compareTo(object) == 0);
  }
}

First, instances of the class can be written out and compared with other instances - the class inherits from Serializable and Comparable. Enabling serialization is virtually transparent, while comparability just requires implementation of a compareTo method. Next, the basis for units is set into place. The elapsed time is kept in milliseconds, as that is the lowest common denominator in Java’s Date class. True, a java.sql.Date records nanoseconds, but my class was never modified to manage more than the resolution in java.util.Date. Milliseconds, seconds, minutes, hours, days and weeks are covered, but weeks are optional and off by default - many applications required days as the coarsest resolution - only a few needed weeks. A switch at the class level allows weeks to be turned on or off for new instances, and a switch at the instance level sets the preference of the instance. Years and months are left out on purpose because their duration is not absolute - years vary in duration because of leap years, and months can have 28, 29, 30 or 31 days - there’s no consensus on their measurement.

There are three constructors: empty, by millisecond, and using another duration. The static between methods for two Date values and two millisecond values may also be used to create an instance. Getters get the duration in qualified elapsed-time value (translated to milliseconds), setters set it similarly with an elapsed-time value or to the same value as another duration. A few extra getters are also available: getPart returns the given part (like the minutes part or the hours part), and getIn returns the full number value in the specified unit (9000 seconds for 2.5 hours, for example). Accumulation is done using the add method, adding an elapsed-time value or the value of another duration. The qualified elapsed-time values are just transformations into milliseconds: 3 hours, 10 minutes, 52.53 seconds would be translated to 11,452,530 milliseconds.

Rendering is just a toString method with a specification of the highest whole units being passed in (days by default, weeks if they’re enabled). Creating and using a DurationFormat object could certainly augment the object, but I never needed to take it that far. Rounding is handled by a standard technique: biasing the value, removing precision and then promoting it back. All in all, a good sturdy class with enough configurability and access methods to make it generally useful, all designed to complement Dates.

Now enters Ruby

I decided to take the transformation a step at a time, starting with a direct Ruby port of my Java Duration class, and refactoring the code in successive iterations into a more Ruby-like state. This was surprisingly easy to do as there’s not really anything in Java that Ruby can’t handle - although a good refactoring editor for Ruby would have been quite useful. (It’ll happen soon enough, I’m sure.) During the iterations, I also looked at my old non-Java code for Duration and other elapsed-time functions to get the insight I needed for better overall results.

Right from the start things felt a little weird because I’m not yet a native Ruby speaker, but somehow more natural as well. Complementing Ruby’s Time class was my goal, and getting to deal with time values in seconds instead of milliseconds was quite comfortable. Since I generally think in whole seconds with fractional amounts, and Ruby uses whole seconds with fractional amounts, no more having to use milliseconds made me happy. Interestingly, Time in Ruby has microsecond resolution instead of milliseconds because underlying Ruby’s Time is the good old C tm structure. So to coexist in Ruby more naturally, I modified the class to be seconds-centric with a resolution to microseconds, extended the list of constants to include microseconds and adjusted the different methods to take in and convert to and from seconds instead of milliseconds.

While I was looking at the unit constants, I knew that in my pre-Java elapsed time code I’d used an enumeration. Java 1.4 and it’s predecessors didn’t contain enumerations, static final constants being the preferred mechanism instead. The base Ruby doesn’t come with enumerations either, but a quick Google search led me to something that astonished me. I knew that Ruby allowed you to modify classes at runtime - to add and remove methods and variables at the class and instance levels - but to my surprise I found a small chunk of code that Brian Schrer had responded to an email with in which someone had asked how enumerations could be done. Brian extended the Object class with enum and bitwise_enum methods. By using this, any class that inherits from Object (and that’s all of them) can do enumeration - effectively extending the language. I encapsulated his Ruby into a file and turned my Java static final constants into a Ruby enum.

The next thing I considered was Ruby argument defaulting. One of the few things I really liked about C++ was the ability to easily specify defaults on functions arguments. In Java this can be especially painful: you either end up with a lot of method variants (a pain for the class developer), calling general-purpose methods with a lot of arguments (a pain for the class user), or calling a lot of small methods (a dangerous pain if they have to be called in a particular sequence or if something important accidentally gets left out.) In the definition of a Ruby method, the ability to assign a default to an argument reduces the pain considerably. In fact if all the arguments have been defaulted, nothing more than the method’s name needs to be specified to make a call. By picking good defaults, classes can be used with ease - using them in the future can be much simpler. Thus with power like this, the trick becomes picking these good defaults - for the software designer, serious user-empathy is required.

Almost all of the methods in my Java code that had to do with passing and retrieving elapsed-time values also included units - and in the places they didn’t, milliseconds were always assumed. In Ruby I can specify the units on all of my elapsed-time methods with seconds as the standard default, leaving the user free to ignore them altogether unless something other than seconds are used. However, I decided to leave the usec method (to return the seconds fraction as whole microseconds, as the Time class does) and add a usecs method, returning the value in microseconds. Technically I didn’t have to have usecs, but I’ve found this sort of method useful in the past. The thought occurred that generalizing units throughout Duration might be useful, but after further consideration, I felt that would be too big a break from the Time class.

Since Ruby is a dynamic language, its nature is to dispense with static typing: everything is just objects with a rough consensus of convention overlying them. If the Ruby object you’re interested in responds to a method, you trust that Ruby conventions have been followed and that what you’ve requested from the object is what you intended. It’s convention like this over a configuration like static typing and interfaces that make Ruby simpler. Take the to_seconds method, for example - I created a static to_seconds method using some of these conventions to transform its object argument. If the object responds to the seconds method, the value of calling the method on the object is returned. Otherwise, if the object responds to usecs, the value is converted to seconds and returned. Otherwise, If the object being passed in responds to the to_f method, conventionally returning a floating point value, that value returned. If none of these conditions are true, the object is assumed to be okay by itself and is returned. The conversion is lightweight and I use it when incoming values are received - in instance methods, class methods and constructors. The beauty of this is that the user can send units or not, or send numbers or not, and by convention everything will come together. And if it doesn’t, you find out in unit testing.

A quick few words about unit testing: Just do it. Ruby has a set of easy-to-use unit testing objects that work along the lines of Java’s JUnit. You write regular Ruby methods with assertions and build up test cases and test suites. Then when you make a change, you run your unit tests, quickly fix whatever is wrong, add more tests and move forward. This keeps you from breaking your code, and as a side benefit, shows others how to use it. It may seem like extra work, but it pays off very quickly. It was at this point I decided to write some unit tests for Duration; I hadn’t written any for the Java code because of it’s evolutionary past, but my Ruby code now has a good test suite that I’ll be able to certify with whenever I make changes.

One of the guiding principles of Ruby design is Don’t Repeat Yourself or DRY, for short. The idea is a really good one. By repeated application of the DRY method in refactoring, methods are decomposed into smaller and smaller constituent methods. In this spirit, good Ruby objects typically have many short methods that each do a simple thing very well. More complex methods are composed of coordinated calls to these simple methods, and so on up the chain. It’s a variation on the old problem solving strategy: divide and conquer. An argument might be made that performance suffers when you write this way, and though this might be true in very compute-heavy applications, in those cases you’re probably not going to write code in either Java or Ruby (at least for now). Interpretation at any level (including in Java byte code or Ruby script) is still not as fast as something closer to the machine. (Just so you know, Ruby is implemented in C and so gives you an escape hatch into high-performance if you really need it.) By adhering to the DRY philosophy, you’ll find yourself with more easily-maintainable code that works well.

Looking over my code with DRY and convention in mind, the Java getIn and getPart methods were renamed and recoded into truncate and part. The truncate change was a no-brainer - by convention that’s what truncate does to a value. The part method returns the upward-clipped and downward-truncated value. These and the to_i method (that returns seconds part, by convention), usec (that returns microseconds part, by convention), usecs (an unconventional shorthand) and to_s (that returns a string rendering, by convention) are all implemented at successively lower levels using to_f (that returns seconds and fractional microseconds, by convention), the % operator and a hash that maps from unit to amount of time in seconds. The thought is that if your Ruby class participates in shared conventions, it will stand a better chance of being used, and used correctly, without confusion.

It’s the same deal with the + and - methods. They return a new instance whose value is the sum or difference of the current Duration with the supplied value. The add and subtract methods return the same things as the + and - methods respectively. But the add! and subtract! methods are different - they change the calling object’s value. By Ruby convention, methods that end with an exclamation point change the object in place if there would otherwise be confusion. The round and round! methods also exhibit this behavior. And interrogative methods - methods that return true or false - typically end in a question mark, as we see in the use_weeks? and uses_weeks? methods. But there are no hard and fast rules: conventions are still just guidelines. For instance, not all methods that change the object have the exclamation point: the set method, for instance, implies the change though it’s name. The exclamation point and question mark suffixes just help make code easier to understand.

The last tidbit is the funny looking <=> method (the spaceship). This is the comparison operation that the Comparable mixin is looking for. By defining it and including the Comparable mixin, you get the mathematic relationship operators <, <=, ==, >, and >= defined in your class for free. Of course, you could always define one of your own variants of these methods, but you just need to do it after you’ve defined <=>. Ruby reads the code it executes sequentially, and you’re free to redefine anything you’d like, and it will happen in the context of what’s transpired to that point. Remember, dynamic interpretation not static compilation: in Ruby, your software’s meta-level is as fluid as your domain level.

The flavor of the object is now Ruby, and it feels about right.

# duration.rb

require 'enum'

class Duration
  include Comparable

  enum %w(MICROS MILLIS SECONDS MINUTES HOURS DAYS WEEKS)

  def Duration.use_weeks?
    @@use_weeks
  end

  def Duration.use_weeks=(use_weeks)
    @@use_weeks = use_weeks
  end

  @@use_weeks = false

  def initialize(value = 0, unit = SECONDS, uses_weeks = @@use_weeks)
    set(value,unit)
    @uses_weeks = uses_weeks
  end

  def uses_weeks=(uses_weeks)
    @uses_weeks = uses_weeks
  end

  def uses_weeks?
    @uses_weeks
  end

  attr_accessor :seconds

  def Duration.to_seconds(value)
    if (value.respond_to? :seconds)
      value = value.seconds
    elsif (value.respond_to? :usecs)
      value = value.usecs/Duration.micros(SECONDS)
    elsif (value.respond_to? :to_f)
      value = value.to_f
    end
    value
  end

  @@seconds = { WEEKS => 60*60*24*7, DAYS => 60*60*24,
       HOURS => 60*60, MINUTES => 60,
       SECONDS => 1, MILLIS => 0.001, MICROS => 0.000001 }

  def Duration.seconds (unit)
    @@seconds[unit]
  end

  def add!(value, unit = SECONDS)
    @seconds += (Duration.to_seconds(value) * Duration.seconds(unit))
    self
  end

  def add(value, unit = SECONDS)
    (duration = Duration.new(@seconds)).add!(value,unit)
  end

  def +(value, unit = SECONDS)
    add(value,unit)
  end

  def subtract!(value, unit = SECONDS)
    @seconds -= (Duration.to_seconds(value) * Duration.seconds(unit))
    self
  end

  def subtract(value, unit = SECONDS)
    (duration = Duration.new(@seconds)).subtract!(value,unit)
  end

  def -(value, unit = SECONDS)
    subtract(value,unit)
  end

  def set(value, unit = SECONDS)
    @seconds = (Duration.to_seconds(value) * Duration.seconds(unit))
    self
  end

  def part(unit)
    seconds = Duration.seconds(unit)
    case unit
      when WEEKS   then (@seconds/seconds).truncate
      when DAYS    then (@uses_weeks ?
                         ((@seconds/seconds)%7).truncate :
                          (@seconds/seconds).truncate)
      when HOURS   then ((@seconds/seconds)%24).truncate
      when MINUTES then ((@seconds/seconds)%60).truncate
      when SECONDS then ((@seconds/seconds)%60).truncate
      when MILLIS  then ((@seconds/seconds)%1000).truncate
      when MICROS  then ((@seconds/seconds)%1000).truncate
      else         0
    end
  end

  def truncate(unit = SECONDS)
    to_f(unit).truncate
  end

  def to_i(unit = SECONDS)
    truncate(unit)
  end

  def to_f(unit = SECONDS)
    @seconds/Duration.seconds(unit)
  end

  def usec
    truncate(MICROS)
  end

  def usecs
    to_f(MICROS)
  end

  def to_s(unit = (@uses_weeks ? WEEKS : DAYS))
    buffer = ""
    value = @seconds
    if value < 0
      @seconds = -value
      buffer += "-"
    end
    buffer +=
      case unit
        when WEEKS
          then
              temp_uses_weeks = @uses_weeks
              @uses_weeks = true
              weeks = sprintf("%d:%d:%02d:%02d:%02d.%03d%03d",
                truncate(WEEKS),part(DAYS),part(HOURS),part(MINUTES),
                part(SECONDS),part(MILLIS),part(MICROS))
              @uses_weeks = temp_uses_weeks
              weeks
        when DAYS
          then sprintf("%d:%02d:%02d:%02d.%03d%03d",
            truncate(DAYS),part(HOURS),part(MINUTES),part(SECONDS),
            part(MILLIS),part(MICROS))
        when HOURS
          then sprintf("%d:%02d:%02d.%03d%03d",
            truncate(HOURS),part(MINUTES),part(SECONDS),part(MILLIS),
            part(MICROS))
        when MINUTES
          then sprintf("%d:%02d.%03d%03d",
            truncate(MINUTES),part(SECONDS),part(MILLIS),part(MICROS))
        when SECONDS
          then sprintf("%d.%03d%03d",
            truncate(SECONDS),part(MILLIS),part(MICROS))
        when MILLIS
          then sprintf("%d.%03d",
            truncate(MILLIS),part(MICROS))
        when MICROS
          then sprintf("%d",
            truncate(MICROS))
        else ""
      end
    @seconds = value
    buffer
  end

  def Duration.between(value0,value1)
    Duration.new((Duration.to_seconds(value1))-(Duration.to_seconds(value0)))
  end

  def round(unit = seconds, fraction = 1)
    Duration.new(@seconds).round!(unit,fraction)
  end

  def round!(unit = seconds, fraction = 1)
    sign = (@seconds < 0)? -1 : (@seconds > 0)? 1 : 0;
    temp = sign * @seconds / Duration.seconds(MICROS)
    micros = (fraction * Duration.seconds(unit) / Duration.seconds(MICROS)).abs
    micros = 1 if (micros < 1)
    @seconds = sign*(((temp+(micros/2))/micros).truncate)*(micros.truncate)*
         Duration.seconds(MICROS)
    self
  end

  def <=>(value)
    @seconds <=> Duration.to_seconds(value)
  end
end
# enum.rb

class Object
  def self.enum(*args)
    args.flatten.each_with_index do |const, i|
      class_eval %(#{const} = #{i})
    end
  end

  def self.bitwise_enum(*args)    
    args.flatten.each_with_index do |const, i|
      class_eval %(#{const} = #{2**i})
    end
  end
end
# tc_duration.rb

require 'test/unit'

require 'duration'

class TC_duration_using_weeks < Test::Unit::TestCase
  def test_using_weeks
    Duration.use_weeks = true
    assert(Duration.use_weeks? == true,"use_weeks class true assignment")
    duration = Duration.new
    assert(duration.uses_weeks? == true,"use_weeks true assignment")
    duration.uses_weeks = false
    assert(duration.uses_weeks? == false,"uses_weeks false reassignment")
    Duration.use_weeks = false
    assert(Duration.use_weeks? == false,"use_weeks class false assignment")
    duration = Duration.new
    assert(duration.uses_weeks? == false,"use_weeks false assignment")
    duration.uses_weeks = true
    assert(duration.uses_weeks? == true,"uses_weeks true reassignment")
  end
end

class TC_duration_new < Test::Unit::TestCase
  def test_new
    duration = Duration.new
    assert(duration.usecs == 0,"new without value is zero")
    duration = Duration.new(0.000001)
    assert(duration.usecs == 1,"new with value is value")
  end
end

class TC_duration_to_seconds < Test::Unit::TestCase
  def test_to_seconds
    assert(Duration.to_seconds(20) == 20,
           "untyped to_seconds is original")
    time = Time.new
    assert(Duration.to_seconds(time) == time.to_f,
           "time to_seconds is original")
    duration = Duration.new(100)
    assert(Duration.to_seconds(duration) == duration.to_f,
           "duration to_seconds is original")
  end
end

class TC_duration_plus < Test::Unit::TestCase
  def test_plus_and_minus
    duration = Duration.new(50)
    assert(duration.to_f == 50,"original")
    duration_pu5 = duration + 5
    assert(duration.to_f == 50,"original after plus 5")
    assert(duration_pu5.to_f == 55,"original plus 5")
    duration_au5 = duration.add 5
    assert(duration.to_f == 50,"original after add 5")
    assert(duration_au5.to_f == 55,"original add 5")
    assert(duration.add!(5) == 55,"original after add! 5")
    duration_tpu5 = Duration.new(55)
    assert(duration_pu5 == duration_tpu5,
           "original plus 5 matches new")
    duration.set(50)
    duration_pd6 = duration + Duration.new(6)
    duration_tpd6 = Duration.new(56)
    assert(duration.to_f == 50,"original after plus duration 6")
    assert(duration_pd6.to_f == 56,"original plus duration 6")
    assert(duration_pd6 == duration_tpd6,
           "original plus duration 6 matches new")
    duration_mu5 = duration - 5
    assert(duration.to_f == 50,"original after minus 5")
    assert(duration_mu5.to_f == 45,"original minus 5")
    duration_su5 = duration.subtract 5
    assert(duration.to_f == 50,"original after subtract 5")
    assert(duration_su5.to_f == 45,"original subtract 5")
    assert(duration.subtract!(5) == 45,"original after subtract! 5")
    duration_tmu5 = Duration.new(45)
    assert(duration_mu5 == duration_tmu5,
           "original minus 5 matches new")
    duration.set(50)
    duration_md6 = duration - Duration.new(6)
    duration_tmd6 = Duration.new(44)
    assert(duration.to_f == 50,"original after minus duration 6")
    assert(duration_md6.to_f == 44,"original minus duration 6")
    assert(duration_md6 == duration_tmd6,
           "original minus 6 duration matches new")
  end
end

class TC_duration_part < Test::Unit::TestCase
  def test_duration_part
    duration = Duration.new
    duration.add!(1,Duration::WEEKS)
    duration.add!(2,Duration::DAYS)
    duration.add!(3,Duration::HOURS)
    duration.add!(4,Duration::MINUTES)
    duration.add!(5,Duration::SECONDS)
    duration.add!(6,Duration::MILLIS)
    duration.add!(7,Duration::MICROS)
    assert(duration.part(Duration::WEEKS) == 1,"week part")
    assert(duration.part(Duration::DAYS) == 9,"day part")
    duration.uses_weeks = true
    assert(duration.part(Duration::DAYS) == 2,"day part")
    assert(duration.part(Duration::HOURS) == 3,"hour part")
    assert(duration.part(Duration::MINUTES) == 4,"minute part")
    assert(duration.part(Duration::SECONDS) == 5,"second part")
    assert(duration.part(Duration::MILLIS) == 6,"milli part")
    assert(duration.part(Duration::MICROS) == 7,"micro part")
  end
end

class TC_duration_truncate < Test::Unit::TestCase
  def test_duration_truncate
    duration = Duration.new
    duration.add!(1,Duration::WEEKS)
    duration.add!(2,Duration::DAYS)
    duration.add!(3,Duration::HOURS)
    duration.add!(4,Duration::MINUTES)
    duration.add!(5,Duration::SECONDS)
    duration.add!(6,Duration::MILLIS)
    duration.add!(7,Duration::MICROS)
    assert(duration.truncate(Duration::WEEKS) == 1,
	 "week truncate")
    assert(duration.truncate(Duration::DAYS) == 9,
	 "day truncate")
    assert(duration.truncate(Duration::HOURS) == 219,
	 "hour truncate")
    assert(duration.truncate(Duration::MINUTES) == 13144,
	 "minute truncate")
    assert(duration.truncate(Duration::SECONDS) == 788645,
	 "second truncate")
    assert(duration.truncate(Duration::MILLIS) == 788645006,
	 "milli truncate")
    assert(duration.truncate(Duration::MICROS) == 788645006007,
	 "micro truncate")
  end
end

class TC_duration_between  < Test::Unit::TestCase
  def test_duration_between
    duration1 = Duration.between(50,90)
    duration2 = Duration.new(50)
    duration3 = Duration.new(91)
    duration4 = Duration.between(duration2,duration3)
    time1 = Time.now
    sleep(0.000001)    
    time2 = Time.now
    duration5 = Duration.between(time1,time2)
    timecount = (time2.to_f)-(time1.to_f)
    assert(duration1.to_f == 40,"direct between setting")
    assert(duration4.to_f == 41,"duration between setting")
    assert(duration5.to_f == timecount,"time between setting")
  end
end

class TC_duration_round  < Test::Unit::TestCase
  def test_duration_round
    duration1 = Duration.new(34.44)
    duration2 = duration1.round(Duration::SECONDS)
    duration3 = duration1.round(Duration::MINUTES)
    duration4 = duration1.round(Duration::SECONDS,0.5)
    duration1.round!(Duration::MILLIS,100)
    assert(duration2.to_f == 34,"whole round")
    assert(duration3.to_f == 60,"up round")
    assert(duration4.to_f == 34.5,"down round")
    assert(duration1.to_f == 34.4,"part round")
  end
end

class TC_duration_compare  < Test::Unit::TestCase
  def test_duration_compare
    duration1 = Duration.new(35)
    duration2 = Duration.new(50)
    assert((duration1 < duration2) == true,"compare less than")
    assert((duration1 <= duration2) == true,"compare less than or equal")
    assert((duration1 == duration2) == false,"compare equal")
    assert((duration1 >= duration2) == false,"compare greater than or equal")
    assert((duration1 > duration2) == false,"compare greater than")
  end
end

class TC_duration_to_s < Test::Unit::TestCase
  def test_duration_to_s
    duration = Duration.new(788645.006007)
    assert(duration.to_s(Duration::WEEKS) == "1:2:03:04:05.006007",
           "to_s WEEKS")
    assert(duration.to_s(Duration::DAYS) == "9:03:04:05.006007",
           "to_s DAYS")
    assert(duration.to_s(Duration::HOURS) == "219:04:05.006007",
           "to_s HOURS")
    assert(duration.to_s(Duration::MINUTES) == "13144:05.006007",
           "to_s MINUTES")
    assert(duration.to_s(Duration::SECONDS) == "788645.006007",
           "to_s SECONDS")
    assert(duration.to_s(Duration::MILLIS) == "788645006.007",
           "to_s MILLIS")
    assert(duration.to_s(Duration::MICROS) == "788645006007",
           "to_s MICROS")
  end
end
# tc_enum.rb

require 'test/unit'

require 'enum'

class TC_enum < Test::Unit::TestCase
  enum %w(A1 A2 A3)

  def test_enum
    assert( [ A1, A2, A3 ] == [ 0, 1, 2 ],"enum value assignments")
  end
end

class TC_bitwise_enum < Test::Unit::TestCase
  bitwise_enum %w(B1 B2 B3)

  def test_bitwise_enum
    assert( [ B1, B2, B3 ] == [ 1, 2, 4 ],"bitwise_enum value assignment")
  end
end

The conventions have been followed, and the Duration object is complete, with supporting classes and (exhaustive) test cases. There’s a lot of meat on these bones - perhaps that’s the only thing that doesn’t feel Ruby-like to me. From what I’ve read and experienced about Ruby, and from the experiences I’ve been told about by other developers, what’s usually written is only on an as-needed basis, without any extra functionality. Lean code is what Ruby is all about. I guess I consider filling the object out as part of wearing the library-developer hat, rather than the program-developer hat. One can wear both, but not at the same time - I switch them often, but they do have to be switched deliberately. For this exercise, I’ve got my library hat on.

One thing that immediately jumped out at me is the difference in the amount of code and the versatility of the objects. The amount of code I had to write in Ruby was significantly smaller than the java - about a third less. The Ruby methods are shorter and simpler making them easier to maintain. And by using duck typing to convert values the code is much more versatile - any object with a seconds, usecs, or to_f method or is a kind of numeric value (tried in that order) can be used to create a duration, now by my own convention.

The biggest contributors to code reduction seem to be the notational simplicity of Ruby and the brevity of it’s control structures. Having array and hash notations built into the language, and not having to declare a variable’s type turns out to be a tremendous codesaver. The code is still all there, but it’s expressed much more succinctly. And since the control structures are so clean and are able to be used inline, a lot of temporary assignments and setup was eliminated. Basically I was able to write less code to get more functionality in Ruby.

Stopwatch.java to stopwatch.rb

Using the Duration class, elapsed time can be set, adjusted, compared and rendered easily in any program. But the other side of elapsed time is measurement. Recording durations must be simple and straightforward, as well as handy. The analogous human tool for precise time measurement in the real world is the stopwatch. Again, down through the years, I’ve carried and transformed the guts of a stopwatch object with me, starting with C functions and turning them into an object first in C++, then transforming that to Objective C and finally Java.

// Stopwatch.java

package com.eymiha.util;

import java.util.Date;

public class Stopwatch implements Comparable {
  private Date start, stop;
  private boolean running;
  protected Duration duration;

  public Stopwatch () {
    this(true);
  }

  public Stopwatch (boolean start) {
    try {
      if (start)
        start();
      else
        reset();
    }
    catch (Exception exception) { // shouldn't happen
      System.err.println("Stopwatch creation - internal error");
      exception.printStackTrace(System.err);
    }
  }

  public Stopwatch (Stopwatch stopwatch) {
    start = new Date(stopwatch.start.getTime());
    stop = new Date(stopwatch.stop.getTime());
    duration = new Duration(stopwatch.duration);
    running = stopwatch.running;
  }

  public void start () throws StopwatchException {
    reset();
    if (isRunning())
      throw new StopwatchException("Can't start a running stopwatch");
    start = new Date();
    running = true;
  }

  public Duration stop () throws StopwatchException {
    if (!isRunning())
      throw new StopwatchException("Can't stop a stopped stopwatch");
    stop = new Date();
    running = false;
    duration.add(Duration.between(start,stop));
    return duration;
  }

  public Duration lap () throws StopwatchException {
    if (!isRunning())
      throw new StopwatchException("Can't lap a stopped stopwatch");
    Duration lapDuration = new Duration(duration);
    lapDuration.add(Duration.between(start,stop));
    return lapDuration;
  }

  public Duration currentLap () throws StopwatchException {
    if (!isRunning())
      throw new StopwatchException("Can't lap a stopped stopwatch");
    return Duration.between(start,new Date());
  }

  public void restart () throws StopwatchException {
    if (isRunning())
      throw new StopwatchException("Can't restart a running stopwatch");
    start = new Date();
    running = true;
  }

  public void reset () {
    start = null;
    stop = null;
    duration = new Duration();
    running = false;
  }

  public Date getStart () {
    return start;
  }

  public Date getStop () {
    return stop;
  }

  public Duration getDuration () {
    try {
      return (isRunning())? lap() : duration;
    }
    catch (Exception exception) { // shouldn't happen
      System.err.println("Stopwatch creation - internal error");
      exception.printStackTrace(System.err);
      return new Duration(0);
    }
  }

  public boolean isRunning () {
    return running;
  }

  public void adjust (Duration adjustment) {
    duration.add(adjustment);
  }

  public String toString () {
    return isRunning()? ("started on " + start) : ("total time " + duration);
  }

  public int compareTo (Object o) {
    return getDuration().compareTo(((Stopwatch)o).getDuration());
  }
}
// StopwatchException.java

package com.eymiha.util;

public class StopwatchException extends Exception {
  private static final long serialVersionUID = 1L;

  public StopwatchException (String message) {
    super(message);
  }
}

The code, though simple, remains true to the workings of an old stopwatch I once had. I could stop it, start it, restart it, lap it, and reset it. I also differentiate between a lap and current lap: the lap is a snapshot of the total elapsed time of a running watch, while the current lap gives the elapsed time since the last start or restart. For good measure, I threw in a constructor to sync from another instance, and a way to start a new instance running on creation. Methods to reveal the start time, stop time, elapsed time and state are also present, as well as a renderer and a method to compare two instances. The code is simpler than Duration, but there are some invalid state considerations to worry about.

The potential exists for user error with a stopwatch - that’s typically why it has just a few buttons on the real world version. The methods here are more descriptive, but ultimately more error prone than the click-big-button, click-small-button interface that you’d use for timing a footrace. A StopwatchException class is created alongside Stopwatch to indicate the illegal states: starting or restarting a running Stopwatch or stopping or lapping a stopped Stopwatch. As this is Java, the user has to catch the thrown exceptions when using the class.

Converting the Java Stopwatch to Ruby was done in much the same way as Duration. The class is pretty straightforward code in either Java or Ruby, but again, the brevity of Ruby led to much smaller code that is easier to follow and maintain than the Java - especially through use of the if modifier, a more versatile Ruby assignment operator, and the DRY principle.

The if modifier (the not-if modifier is called unless) can be tacked onto the back of any Ruby statement, seemingly as an afterthought: “Do all this if that.” This leads to nice concise code - as can be seen when I’m raising exceptions. (The Ruby code follows the same real-world-stopwatch logic as the Java code, and the same exceptions are thrown.) The Ruby raise mechanism is also briefer than the corresponding Java, primarily because raise is a method of the Kernel object. The actual mechanism that propagates the exception is hidden, as it also effectively is in Java, but the exception instance creation and the requisite throws decoration on the method are not needed in Ruby.

There may be some confusion for the Java developer trying to go to Ruby when it comes to exception handing. The quick translation is that Java’s try-catch-finally is the same as Ruby’s begin-rescue-ensure, and Java’s throw is the same as Ruby’s raise (and fail). Ruby also has another mechanism: throw-catch. It’s basically a way to break out of current processing flow with a throw, transferring control to the matching catch upstream on the stack. (If you’re an old UNIX-hacker, this is effectively the non-local goto, implemented using the setjmp and longjmp functions.)

The Ruby assignment operator = does a wonderful thing for code brevity. It is one of the few operations built into the language (not a method) and besides just assigning one value to one symbol, it can assign a group of values to a group of symbols, respectively. This allows you to put together a whole lot of assignments in one statement that should all happen together. You might not think this seems like much of a big deal, but when a few things that are related together can be changed in the same operation it can be a boon to code understanding. Anything with interrelated state, values and business rules can be approached much more easily when these sorts of operations can be expressed so cleanly.

Finally, by applying the DRY principle, it was clear that a lot of this state setting could be factored into a few common methods. The internal_copy, internal_start and internal_stop methods were all factored out, and the code in the methods became just plain small. The class ended up being nice, tight and clean.

# stopwatch.rb

require 'duration'

class StopwatchError < StandardError
end

class Stopwatch
  include Comparable

  def initialize(start_running = true)
    @is_running = false
    if start_running
      start
    else
      reset
    end
  end

  def start
    raise(StopwatchError,"Can't start a running stopwatch") if @is_running
    reset
    internal_start
  end

  def stop
    raise(StopwatchError,"Can't stop a stopped stopwatch") if !@is_running
    internal_stop
  end

  def lap
    current_lap.add!(@duration)
  end

  def current_lap
    raise(StopwatchError,"Can't lap a stopped stopwatch") if !@is_running
    Duration.between(@start_time,Time.now)
  end

  def restart
    raise(StopwatchError,"Can't restart a running stopwatch") if @is_running
    internal_start
  end

  def reset
    internal_copy(nil, nil, Duration.new, false)
  end

  attr_reader :start_time, :stop_time

  def is_running?
    @is_running
  end

  def duration
    @is_running ? lap : @duration
  end

  def adjust(adjustment)
    @duration.add!(adjustment)
  end

  def to_s
    @is_running ? "started on #{start_time}" : "total time #{duration}"
  end

  def <=>(value)
    duration <=> value.duration
  end

private

  def internal_copy (start_time, stop_time, duration, is_running)
    @start_time, @stop_time, @duration, @is_running =
        start_time, stop_time, duration, is_running
  end

  def internal_start
    @start_time, @is_running = Time.now, true
  end

  def internal_stop
    @stop_time, @is_running = Time.now, false
    @duration.add!(Duration.between(@start_time,@stop_time))
  end
end

This time I actually started by writing a bunch of unit tests first, so the Ruby design was established before I started writing the class. This is a great way to get things done - do just what is needed to satisfy the tests, with an eye to refactoring.

# tc_stopwatch.rb

require 'test/unit'

require 'stopwatch'

module Stopwatch_assertions
  def assert_stopwatch_consistent(stopwatch)
    if stopwatch.is_running?
      assert((stopwatch.start_time.kind_of? Time),
             "stopwatch has valid start time")
    else
      assert(((stopwatch.start_time.kind_of? Time) &&
              (stopwatch.stop_time.kind_of? Time)) ||
             ((stopwatch.start_time == nil) &&
              (stopwatch.stop_time == nil)),
             "stopwatch has valid start and stop times")
    end
    assert((stopwatch.duration.kind_of? Duration),
        "stopwatch has valid duration")
  end
end

class TC_stopwatch_new < Test::Unit::TestCase
  include Stopwatch_assertions

  def test_stopwatch_new
    stopwatch = Stopwatch.new
    assert(stopwatch.is_running?,"stopwatch is running");
    assert_stopwatch_consistent(stopwatch)
    stopwatch = Stopwatch.new(false)
    assert(!stopwatch.is_running?,"stopwatch is not running");
    assert_stopwatch_consistent(stopwatch)
  end

  def test_stopwatch_start
    stopwatch = Stopwatch.new false
    assert(stopwatch.start_time == nil,"unstarted stopwatch has no start time")
    stopwatch.start
    assert(stopwatch.start_time != nil,"started stopwatch has start time")
    assert_stopwatch_consistent(stopwatch)
  end

  def test_stopwatch_stop
    stopwatch = Stopwatch.new
    assert(stopwatch.stop_time == nil,"unstopped stopwatch has no stop time")
    assert_stopwatch_consistent(stopwatch)
    stopwatch.stop
    assert(stopwatch.stop_time != nil,"stopped stopwatch has stop time")
    assert_stopwatch_consistent(stopwatch)
  end

  def test_stopwatch_restart
    stopwatch = Stopwatch.new
    stopwatch.stop
    stopwatch.restart
    assert(stopwatch.start_time != nil,"restarted stopwatch has start time")
    assert_stopwatch_consistent(stopwatch)    
  end

  def test_stopwatch_duration_laps
    stopwatch = Stopwatch.new false
    assert(stopwatch.duration == 0,"unstarted stopwatch has zero duration")
    stopwatch.start
    sleep 0.05
    assert(stopwatch.duration >= 0.049,
           "reasonable duration on running stopwatch")
    lap = stopwatch.lap
    assert(stopwatch.duration-lap <= 0.01,"lap and duration match")
    stopwatch.stop
    duration = stopwatch.duration
    assert(duration >= 0.049,"reasonable duration on stopped stopwatch")
    sleep 0.05
    assert(duration == stopwatch.duration,
           "no time leakage on stopped stopwatch")
    stopwatch.restart
    sleep 0.05
    assert(duration < stopwatch.duration,
           "resonable duration of restarted stopwatch")
    current_lap = stopwatch.current_lap
    lap = stopwatch.lap
    assert(current_lap >= 0.049,"reasonable current lap of restarted stopwatch")
    assert(lap >= 0.009,"reasonable lap of restarted stopwatch")
    assert(lap > current_lap,
           "valid relationship between lap and current lap of restarted stopwatch")
  end

  def test_stopwatch_reset
    stopwatch = Stopwatch.new
    stopwatch.stop
    stopwatch.reset
    assert(stopwatch.start_time == nil,"reset stopwatch has no start time")
    assert_stopwatch_consistent(stopwatch)    
  end

  def test_stopwatch_running_start
    stopwatch = Stopwatch.new
    assert_raise(StopwatchError,
                 "stopwatch with a running start throws exception") { stopwatch.start }
    stopwatch.stop
    stopwatch.restart
    assert_raise(StopwatchError,
                 "restarted stopwatch with a running start throws exception") { stopwatch.start }
  end

  def test_stopwatch_non_running_stop
    stopwatch = Stopwatch.new false
    assert_raise(StopwatchError,
                 "new non-running stop stopwatch throws an exception") { stopwatch.stop }
    stopwatch.start
    stopwatch.stop
    assert_raise(StopwatchError,
                 "stopwatch with a non-running stop throws an exception") { stopwatch.stop }
  end

  def test_stopwatch_running_restart
    stopwatch = Stopwatch.new
    assert_raise(StopwatchError,
                 "stopwatch with a running start throws exception") { stopwatch.restart }
  end

  def test_stopwatch_non_running_lap
    stopwatch = Stopwatch.new false
    assert_raise(StopwatchError,
                 "stopwatch with a non-running lap throws exception") { stopwatch.lap }
  end

end

Between Duration and Stopwatch, the enum mixin and their unit tests, I now have some full-featured Ruby code to represent and measure elapsed time that I can use on into the future. Feel free to use it yourself if you’d like - and let me know if you add any significant features that I didn’t consider.

Moving forward in Java and Ruby

After going through this exercise, I asked myself, “So is Ruby better than Java?” That’s too big a question. Well then, “Can Ruby replace Java?” It could, but it won’t. Though it can potentially do everything Java can - Ruby certainly has some advantages, especially when it comes to faster coding, and it most certainly has the higher OO ground - there’s too large an installed Java base (and similarly large installed bases of C++ and C#) to be displaced as the preferred industrial language any time soon. (If anyone starts selling COBOL LIVES shirts, I want a cut.) Too many successful companies have paid for too much development to shift away from what they’re currently doing, at least in anything but small increments. Such is the inertia of profits. But that doesn’t mean Ruby won’t make its way into the workplace - it’ll just start small and grow, just as Java, C#, C++ and C did.

Investment in technology is tricky business, and the compelling question that should always be asked first is, “Is it worth the money?” As a software developer who wants the latest and greatest cool languages and tools, “Of course it’s worth it.” As a businessman who wants happy stockholders and happier bosses along with higher revenue and lower costs, but doesn’t want to get blindsided, “Maybe, maybe not… perhaps we should do a pilot to see.” As a paying customer who’s buying products, reading mail, surfing and clicking on web pages, “I don’t care as long as I get better stuff for cheap or free.”

In many cases the benefits of adopting Ruby would be positive because the time needed to develop good applications, put them in place and then enhance and maintain them could be dramatically reduced. The more forward-thinking ten percent of large businesses will gravitate to Ruby more quickly than the rest, and small business owners that pay for custom development may jump if the cost of Ruby-based systems is favorable. Otherwise, expect to be working in established languages and frameworks for a while and get funny looks from your management when you start spouting Ruby heresy. But even then, that doesn’t mean you still can’t secretly experiment with Ruby late at night with the stereo blasting and a towel stuck under the door.

There’s also JRuby to consider. The goal of this open-source effort is to allow bi-directional use of Java and Ruby in the same program. In effect, Java can run Ruby Scripts and Ruby can access Java Objects. The potential for this trans-language cooperation is high, especially since JRuby could get Ruby to a large installed base through the back door. But currently this is still mostly potential; the team has not yet completed a first release, and the project went on a hiatus for a while to regroup. It seems to be coming back with strong support, but only time will tell. And even if JRuby doesn’t meet our hopeful expectations, there are other efforts going on - someone will succeed in bringing things together.

The bottom line again, is that Ruby is worth your time to learn. It most certainly was worth mine, as I find more and more uses for it in my own work. Ruby is compelling, and so is Rails, which I’m currently exploring as well. Even though I’ve had some good exposure, I’m still only a novice - I have a lot left to learn about this language and need to unlearn some of the extra cruft the established languages have thrust upon me.

I must do what I must do - at least Ruby makes me smile while I do it.