Java Working with Roman Numerals

Java Working with Roman Numerals

Problem

You need to format numbers as Roman numerals. Perhaps you’ve just written the
next Titanic or Star Wars episode and you need to get the copyright date correct. Or,
on a more mundane level, you need to format page numbers in the front matter of a
book.

Solution

Use my RomanNumberFormat class:

// RomanNumberSimple.java
RomanNumberFormat nf = new RomanNumberFormat( );
int year = Calendar.getInstance( ).get(Calendar.YEAR);
System.out.println(year + " -> " + nf.format(year));

The use of Calendar to get the current year is explained in Recipe 6.1. Running RomanNumberSimple looks like this:

+ jikes +E -d . RomanNumberSimple.java
+ java RomanNumberSimple
2004 -> MMIV

Explained

Nothing in the standard API formats Roman numerals. However, the java.text. Format class is designed to be subclassed for precisely such unanticipated purposes, so I have done just that and developed a class to format numbers as Roman numer- als. Here is a better and complete example program of using it to format the current year. I can pass a number of arguments on the command line, including a "-" where I want the year to appear (note that these arguments are normally not quoted; the "-" must be an argument all by itself, just to keep the program simple). I use it as follows:

$ java RomanYear
Copyright (c) - Ian Darwin
Copyright (c) MMIV Ian Darwin
$

The code for the RomanYear program is simple, yet it correctly puts spaces around the arguments:

import java.util.*;
/** Print the current year in Roman Numerals */
public class RomanYear {
public static void main(String[] argv) {
RomanNumberFormat rf = new RomanNumberFormat( );
Calendar cal = Calendar.getInstance( );
int year = cal.get(Calendar.YEAR);
// If no arguments, just print the year.
if (argv.length == 0) {
System.out.println(rf.format(year));
return;
}
// Else a micro-formatter: replace "-" arg with year, else print.
for (int i=0; i<argv.length; i++) {
if (argv[i].equals("-"))
System.out.print(rf.format(year));
else
System.out.print(argv[i]);
// e.g., "Copyright"
System.out.print(' ');
}
System.out.println( );
}
}

Now here’s the code for the RomanNumberFormat class. I did sneak in one additional class, java.text.FieldPosition . A FieldPosition simply represents the position of one numeric field in a string that has been formatted using a variant of NumberFormat. format( ) . You construct it to represent either the integer part or the fraction part (of course, Roman numerals don’t have fractional parts). The FieldPosition methods getBeginIndex( ) and getEndIndex( ) indicate where in the resulting string the given field wound up. Example is the class that implements Roman number formatting. As the com- ments indicate, the one limitation is that the input number must be less than 4,000.

Example RomanNumberFormat.java

import java.text.*;
import java.util.*;
/**
* Roman Number class. Not localized, since Latin's a Dead Dead Language
* and we don't display Roman Numbers differently in different Locales.
* Filled with quick-n-dirty algorithms.
*/
public class RomanNumberFormat extends Format {
/** Characters used in "Arabic to Roman", that is, format( ) methods. */
static char A2R[][] = {
{ 0, 'M' },
{ 0, 'C', 'D', 'M' },
{ 0, 'X', 'L', 'C' },
{ 0, 'I', 'V', 'X' },
};

/** Format a given double as a Roman Numeral; just truncate to a
* long, and call format(long).
*/
public String format(double n) {
return format((long)n);
}
/** Format a given long as a Roman Numeral. Just call the
* three-argument form.
*/
public String format(long n) {
if (n <= 0 || n >= 4000)
throw new IllegalArgumentException(n + " must be > 0 && < 4000");
StringBuffer sb = new StringBuffer( );
format(new Integer((int)n), sb, new FieldPosition(NumberFormat.INTEGER
FIELD));
return sb.toString( );
}

/* Format the given Number as a Roman Numeral, returning the
* Stringbuffer (updated), and updating the FieldPosition.
* This method is the REAL FORMATTING ENGINE.
* Method signature is overkill, but required as a subclass of Format.
*/
public StringBuffer format(Object on, StringBuffer sb, FieldPosition fp) {
if (!(on instanceof Number))
throw new IllegalArgumentException(on + " must be a Number object");

if (fp.getField( ) != NumberFormat.INTEGER_FIELD)
throw new IllegalArgumentException(fp +
" must be FieldPosition(NumberFormat.INTEGER_FIELD");
int n = ((Number)on).intValue( );
// TODO check for in range here
// First, put the digits on a tiny stack. Must be 4 digits.
for (int i=0; i<4; i++) {
int d=n%10;
push(d);
// System.out.println("Pushed " + d);
n=n/10;
}

// Now pop and convert.
for (int i=0; i<4; i++) {
int ch = pop( );
// System.out.println("Popped " + ch);
if (ch==0)
continue;
else if (ch <= 3) {
for(int k=1; k<=ch; k++)
sb.append(A2R[i][1]); // I
}
else if (ch == 4) {
sb.append(A2R[i][1]);
// I
sb.append(A2R[i][2]);
// V
}
else if (ch == 5) {
sb.append(A2R[i][2]);
// V
}
else if (ch <= 8) {
sb.append(A2R[i][2]);
// V
for (int k=6; k<=ch; k++)
sb.append(A2R[i][1]);
// I
}
else { // 9
sb.append(A2R[i][1]);
sb.append(A2R[i][3]);
}
}
// fp.setBeginIndex(0);
// fp.setEndIndex(3);
return sb;
}

/** Parse a generic object, returning an Object */
public Object parseObject(String what, ParsePosition where) {
throw new IllegalArgumentException("Parsing not implemented");
// TODO PARSING HERE
// return new Long(0);
}
/* Implement a toy stack */
protected int stack[] = new int[10];
protected int depth = 0;
/* Implement a toy stack */
protected void push(int n) {
stack[depth++] = n;
}
/* Implement a toy stack */
protected int pop( ) {
return stack[--depth];
}
}

Several of the public methods are required because I wanted it to be a subclass of Format , which is abstract. This accounts for some of the complexity, like having three different format methods. Note that the parseObject( ) method is also required, but we don’t actually implement parsing in this version. This is left as the usual exercise for the reader.

0 comments:

Post a Comment