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 Date
s.
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.