Tuesday, May 29, 2007

Hacking JRuby

I recently got to contribute to the JRuby project -- issue #JRUBY-914: "JRuby's BigDecimal needs to round". In doing so, I got to learn a bit about Java's BigDecimal -- particularly how it differs from Java 1.4 to Java 5, implement a bit of code in the JRuby codebase, and test my ideas conveniently and interactively using Jython. Not a bad way to spend a couple hours hacking and contributing to the community.

In a nutshell, java.math.BigDecimal allows you to specify a scale, which is the number of digits to the right of the decimal place. So a BigDecimal with an unscaledValue of 1 and a scale of 1 would evaluate to 0.1. Java 5 allows the scale to take a negative value. So a BigDecimal with an unscaledValue of 1 and a scale of -1 would evaluate to 10. This is significant for rounding because it allows you to round a number to any digit, right or left of the decimal. But Java 1.4 does not allow a negative scale, so we need another approach to allow the same behavior on both versions.

My solution was to use the movePointLeft() and movePointRight() methods of the BigDecimal class to achieve the same result. It turns out those methods do permit a negative offset, so moving the decimal point "right" by a negative number actually moves it to the left, and vice versa. This turns out to be all we need to achieve the desired behavior: moving the decimal point to the right scale times positions the digit to be rounded to just to the left of the decimal point. We can then round normally, using whichever Java rounding mode suits our needs. Then we shift the decimal point left scale times to restore the (now rounded) number.

Note that this algorithm works for both negative and positive values of scale. It's a little bit of overkill when scale is non-negative, though, so I just implemented it for the interesting case of scale < 0.