001package com.hfg.math;
002
003
004import java.util.ArrayList;
005import java.util.List;
006
007import com.hfg.exception.InvalidValueException;
008
009
010//------------------------------------------------------------------------------
011/**
012 Roman numeral representation. The current implementation works for numbers up to 4000.
013 <div class='example'>
014 Ex: given <code style='font-style:italic'> RomanNumeral num = new RomanNumeral(1944);</code>
015 <p>
016 <code style='font-style:italic'>num.toString()</code> will be equal to "MCMXLIV".
017 </p>
018 </div>
019 <div>
020 See <a href='http://en.wikipedia.org/wiki/Roman_numerals'>http://en.wikipedia.org/wiki/Roman_numerals</a>
021 </div>
022 @author J. Alex Taylor, hairyfatguy.com
023 */
024//------------------------------------------------------------------------------
025// com.hfg XML/HTML Coding Library
026//
027// This library is free software; you can redistribute it and/or
028// modify it under the terms of the GNU Lesser General Public
029// License as published by the Free Software Foundation; either
030// version 2.1 of the License, or (at your option) any later version.
031//
032// This library is distributed in the hope that it will be useful,
033// but WITHOUT ANY WARRANTY; without even the implied warranty of
034// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
035// Lesser General Public License for more details.
036//
037// You should have received a copy of the GNU Lesser General Public
038// License along with this library; if not, write to the Free Software
039// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
040//
041// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com
042// jataylor@hairyfatguy.com
043//------------------------------------------------------------------------------
044// TODO: Add support for values 4000+ that need a macron (bar above the letter) for display.
045//       The macron indicates that the number is multiplied by 1000.
046
047public class RomanNumeral extends Number implements Comparable<RomanNumeral>
048{
049   private String mString;
050   private int    mValue;
051
052   private static List<RomanNumeralSymbol> sHiToLowSymbols = new ArrayList<RomanNumeralSymbol>(7);
053
054   static
055   {
056      sHiToLowSymbols.add(RomanNumeralSymbol.M);
057      sHiToLowSymbols.add(RomanNumeralSymbol.D);
058      sHiToLowSymbols.add(RomanNumeralSymbol.C);
059      sHiToLowSymbols.add(RomanNumeralSymbol.L);
060      sHiToLowSymbols.add(RomanNumeralSymbol.X);
061      sHiToLowSymbols.add(RomanNumeralSymbol.V);
062      sHiToLowSymbols.add(RomanNumeralSymbol.I);
063   }
064
065   //**************************************************************************
066   // CONSTRUCTORS
067   //**************************************************************************
068
069   //--------------------------------------------------------------------------
070   /**
071    Create a RomanNumeral object from an integer value.
072    */
073   public RomanNumeral(int inValue)
074   {
075      super();
076      calculateString(inValue);
077   }
078
079   //--------------------------------------------------------------------------
080   /**
081    Create a RomanNumeral object from a Roman numeral string such as "MCMXLIV".
082    */
083   public RomanNumeral(String inValue)
084   {
085      super();
086      parseString(inValue);
087   }
088
089
090   //**************************************************************************
091   // PUBLIC METHODS
092   //**************************************************************************
093
094   //--------------------------------------------------------------------------
095   /**
096    Returns the Roman numeral string value of the Roman numeral.
097    */
098   @Override
099   public String toString()
100   {
101      return mString;
102   }
103
104   //--------------------------------------------------------------------------
105   /**
106    Returns the integer value of the Roman numeral.
107    @deprecated use intValue()
108    */
109   public int getIntValue()
110   {
111      return mValue;
112   }
113   //--------------------------------------------------------------------------
114   @Override
115   public int intValue()
116   {
117      return mValue;
118   }
119
120   //--------------------------------------------------------------------------
121   @Override
122   public long longValue()
123   {
124      return mValue;
125   }
126
127   //--------------------------------------------------------------------------
128   @Override
129   public float floatValue()
130   {
131      return mValue;
132   }
133
134   //--------------------------------------------------------------------------
135   @Override
136   public double doubleValue()
137   {
138      return mValue;
139   }
140
141   //--------------------------------------------------------------------------
142   /**
143    Numerically compares two RomanNumeral objects.
144    */
145   public int compareTo(RomanNumeral inObj)
146   {
147      return NumUtil.compare(intValue(), inObj.intValue());
148   }
149 
150   //**************************************************************************
151   // PRIVATE METHODS
152   //**************************************************************************
153
154   //--------------------------------------------------------------------------
155   /*
156   From wikipedia:
157   <br /><br />
158   Subtractive principle
159   <br /><br />
160   Generally, Roman numerals are written in descending order from left to right,
161   and are added sequentially, for example MMVI (2006) is interpreted as 1000 + 1000 + 5 + 1.
162   <br /><br />
163   Certain combinations employ a subtractive principle, which specifies that where a symbol
164   of smaller value precedes a symbol of larger value, the smaller value is subtracted from
165   the larger value, and the result is added to the total. For example, in MCMXLIV (1944), t
166   he symbols C, X and I each precede a symbol of higher value, and the result is interpreted as
167   1000 plus (1000 minus 100) plus (50 minus 10) plus (5 minus 1).
168   <br /><br />
169   A numeral for 10n (I, X, or C) may not precede a numeral larger than 10n+1, where n is an integer.
170   That is, I may precede V and X, but not L or C; X may precede L or C, but not D or M.
171   The numerals 5?10n (V, L, or D) may not be followed by a numeral of greater or equal value.
172   Any symbol that appears more than once consecutively may not be followed by a symbol of larger value.
173    */
174   private void calculateString(int inValue)
175   {
176      if (inValue < 1)
177      {
178         throw new InvalidValueException("Roman numerals cannot be less than or equal to zero!");
179      }
180      else if (inValue > 3999)
181      {
182         throw new InvalidValueException("Roman numerals greater than 3999 are not currently supported!");
183      }
184
185      StringBuilder buffer = new StringBuilder();
186      int remainingValue = inValue;
187      while (remainingValue > 0)
188      {
189         for (int i = 0; i < sHiToLowSymbols.size(); i++)
190         {
191            RomanNumeralSymbol symbol = sHiToLowSymbols.get(i);
192
193            if (symbol.getIntValue() > remainingValue)
194            {
195               // Is a subtractive combination appropriate?
196               for (int j = sHiToLowSymbols.size() - 1; j > i; j--)
197               {
198                  RomanNumeralSymbol lowerSymbol = sHiToLowSymbols.get(j);
199
200                  if (lowerSymbol != null
201                      && symbol.getIntValue() - lowerSymbol.getIntValue() <= remainingValue
202                      && lowerSymbol.canPrecede(symbol))
203                  {
204                     buffer.append(lowerSymbol.getLetter());
205                     buffer.append(symbol.getLetter());
206                     remainingValue -= (symbol.getIntValue() - lowerSymbol.getIntValue());
207                     break;
208                  }
209               }
210            }
211            else
212            {
213               while (remainingValue >= symbol.getIntValue())
214               {
215                  buffer.append(symbol.getLetter());
216                  remainingValue -= symbol.getIntValue();
217               }
218
219               if (remainingValue > 0) i--;
220            }
221         }
222      }
223
224      mString = buffer.toString();
225      mValue = inValue;
226   }
227
228   //--------------------------------------------------------------------------
229   private void parseString(String inString)
230   {
231      RomanNumeralSymbol previousSymbol = null;
232      mValue = 0;
233      for (char theChar : inString.toCharArray())
234      {
235         RomanNumeralSymbol symbol = RomanNumeralSymbol.valueOf(theChar);
236         if (null == symbol)
237         {
238            throw new InvalidValueException("'" + theChar + "' is not a recognized Roman numeral symbol!");
239         }
240
241         mValue += symbol.getIntValue();
242         if (previousSymbol != null
243             && symbol.getIntValue() > previousSymbol.getIntValue())
244         {
245            mValue -= 2 * previousSymbol.getIntValue();
246         }
247
248         previousSymbol = symbol;
249      }
250
251      mString = inString;
252   }
253}