Recipe 5.11 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

Discussion

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 numerals. 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 5-2 is the class that implements Roman number formatting. As the comments indicate, the one limitation is that the input number must be less than 4,000.

Example 5-2. 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         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 +          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.

See Also

Java I/O (O'Reilly) has an entire chapter on NumberFormat and develops an ExponentialNumberFormat subclass.

The online source for this book has ScaledNumberFormat , which prints numbers with a maximum of four digits and a computerish scale factor (B for bytes, K for kilo-, M for mega-, and so on).



Java Cookbook
Java Cookbook, Second Edition
ISBN: 0596007019
EAN: 2147483647
Year: 2003
Pages: 409
Authors: Ian F Darwin

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net