View Javadoc

1   /*
2    * Copyright 2006 Robert Hanson <iamroberthanson AT gmail.com>
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.gwtwidgets.client.util;
18  
19  /***
20   * <dl>
21   * <dt><b>Title: </b><dd>Decimal Format</dd>
22   * <p>
23   * <dt><b>Description: </b><dd>This is a simple number formatting/ parsing class. Besides the simple number formatting
24   * it also interprets shortcuts for thousand (k) million (m) and billion (b).<p/>
25   * This Number Format class was adapted from the public domain javascript class found at 
26   * http://www.mredkj.com/javascript/nfdocs.html </dd>
27   * <p>
28   * </dl>
29   * @author <a href="mailto:jasone@greenrivercomputing.com">Jason Essington</a>
30   * @version $Revision: 0.0 $
31   */
32  
33  public class NumberFormat
34  {
35      public static final String COMMA = ",";
36      public static final String PERIOD = ".";
37      public static final char DASH = '-';
38      public static final char LEFT_PAREN = '(';
39      public static final char RIGHT_PAREN = ')';
40  
41      // k/m/b Shortcuts
42      public static final String THOUSAND = "k";
43      public static final String MILLION = "m";
44      public static final String BILLION = "b";
45      
46      // currency position constants
47      public static final int CUR_POS_LEFT_OUTSIDE = 0;
48      public static final int CUR_POS_LEFT_INSIDE = 1;
49      public static final int CUR_POS_RIGHT_INSIDE = 2;
50      public static final int CUR_POS_RIGHT_OUTSIDE = 3;
51  
52      // negative format constants
53      public static final int NEG_LEFT_DASH = 0;
54      public static final int NEG_RIGHT_DASH = 1;
55      public static final int NEG_PARENTHESIS = 2;
56  
57      // constant to signal that fixed precision is not to be used
58      public static final int ARBITRARY_PRECISION = -1;
59  
60      private String inputDecimalSeparator = PERIOD; // decimal character used on the original string
61  
62      private boolean showGrouping = true;
63      private String groupingSeparator = COMMA; // thousands grouping character
64      private String decimalSeparator = PERIOD; // decimal point character
65  
66      private boolean showCurrencySymbol = false;
67      private String currencySymbol = "$";
68      private int currencySymbolPosition = CUR_POS_LEFT_OUTSIDE;
69  
70      private int negativeFormat = NEG_LEFT_DASH;
71      private boolean isNegativeRed = false; // wrap the output in html that will display red?
72  
73      private int decimalPrecision = 0;
74      private boolean useFixedPrecision = false;
75      private boolean truncate = false; // truncate to decimalPrecision rather than rounding? 
76  
77      private boolean isPercentage = false; // should the result be displayed as a percentage?
78  
79      private NumberFormat()
80      {
81      }
82  
83      /***
84       * returns the default instance of NumberFormat
85       * @return
86       */
87      public static NumberFormat getInstance ()
88      {
89          NumberFormat nf = new NumberFormat();
90          return nf;
91      }
92  
93      /***
94       * Returns a currency instance of number format
95       * @return
96       */
97      public static NumberFormat getCurrencyInstance ()
98      {
99          return getCurrencyInstance("$", true);
100     }
101 
102     /***
103      * Returns a currency instance of number format that uses curSymbol as the currency symbol
104      * @param curSymbol
105      * @return
106      */
107     public static NumberFormat getCurrencyInstance (String curSymbol)
108     {
109         return getCurrencyInstance(curSymbol, true);
110     }
111 
112     /***
113      * Returns a currency instance of number format that uses curSymbol as the currency symbol 
114      * and either commas or periods as the thousands separator.
115      * @param curSymbol Currency Symbol
116      * @param useCommas true, uses commas as the thousands separator, false uses periods
117      * @return
118      */
119     public static NumberFormat getCurrencyInstance (String curSymbol, boolean useCommas)
120     {
121         NumberFormat nf = new NumberFormat();
122         nf.isCurrency(true);
123         nf.setCurrencySymbol(curSymbol);
124         if (!useCommas) {
125             nf.setDecimalSeparator(COMMA);
126             nf.setGroupingSeparator(PERIOD);
127         }
128         nf.setFixedPrecision(2);
129         return nf;
130     }
131 
132     /***
133      * Returns an instance that formats numbers as integers.
134      * @return
135      */
136     public static NumberFormat getIntegerInstance ()
137     {
138         NumberFormat nf = new NumberFormat();
139         nf.setShowGrouping(false);
140         nf.setFixedPrecision(0);
141         return nf;
142     }
143 
144     public static NumberFormat getPercentInstance ()
145     {
146         NumberFormat nf = new NumberFormat();
147         nf.isPercentage(true);
148         nf.setFixedPrecision(2);
149         nf.setShowGrouping(false);
150         return nf;
151     }
152 
153     public String format (String num)
154     {
155         return toFormatted(parse(num));
156     }
157 
158     public double parse (String num)
159     {
160         return asNumber(num, inputDecimalSeparator);
161     }
162 
163     /***
164      * Static routine that attempts to create a double out of the
165      * supplied text. This routine is a bit smarter than Double.parseDouble()
166      * @param num
167      * @return
168      */
169     public static double parseDouble (String num, String decimalChar)
170     {
171         return asNumber(num, decimalChar);
172     }
173 
174     public static double parseDouble (String num)
175     {
176         return parseDouble(num, PERIOD);
177     }
178 
179     public void setInputDecimalSeparator (String val)
180     {
181         inputDecimalSeparator = val == null ? PERIOD : val;
182     }
183 
184     public void setNegativeFormat (int format)
185     {
186         negativeFormat = format;
187     }
188 
189     public void setNegativeRed (boolean isRed)
190     {
191         isNegativeRed = isRed;
192     }
193 
194     public void setShowGrouping (boolean show)
195     {
196         showGrouping = show;
197     }
198 
199     public void setDecimalSeparator (String separator)
200     {
201         decimalSeparator = separator;
202     }
203 
204     public void setGroupingSeparator (String separator)
205     {
206         groupingSeparator = separator;
207     }
208 
209     public void isCurrency (boolean isC)
210     {
211         showCurrencySymbol = isC;
212     }
213 
214     public void setCurrencySymbol (String symbol)
215     {
216         currencySymbol = symbol;
217     }
218 
219     public void setCurrencyPosition (int cp)
220     {
221         currencySymbolPosition = cp;
222     }
223 
224     public void isPercentage (boolean pct)
225     {
226         isPercentage = pct;
227     }
228 
229     /***
230      * Sets the number of fixed precision decimal places should be displayed.
231      * To use arbitrary precision, setFixedPrecision(NumberFormat.ARBITRARY_PRECISION)
232      * @param places 
233      */
234     public void setFixedPrecision (int places)
235     {
236         useFixedPrecision = places != ARBITRARY_PRECISION;
237         this.decimalPrecision = places < 0 ? 0 : places;
238     }
239 
240     /***
241      * Causes the number to be truncated rather than rounded to its fixed precision.
242      * @param trunc
243      */
244     public void setTruncate (boolean trunc)
245     {
246         truncate = trunc;
247     }
248 
249     /***
250      * 
251      * @param preSep raw number as text
252      * @param PERIOD incoming decimal point
253      * @param decimalSeparator outgoing decimal point
254      * @param groupingSeparator thousands separator
255      * @return
256      */
257     private String addSeparators (String preSep)
258     {
259         String nStr = preSep;
260         int dpos = nStr.indexOf(PERIOD);
261         String nStrEnd = "";
262         if (dpos != -1) {
263             nStrEnd = decimalSeparator + nStr.substring(dpos + 1, nStr.length());
264             nStr = nStr.substring(0, dpos);
265         }
266         int l = nStr.length();
267         for (int i = l; i > 0; i--) {
268             nStrEnd = nStr.charAt(i - 1) + nStrEnd;
269             if (i != 1 && ((l - i + 1) % 3) == 0) nStrEnd = groupingSeparator + nStrEnd;
270         }
271         return nStrEnd;
272     }
273 
274     protected String toFormatted(double num)
275     {
276         String nStr;
277 
278         if (isPercentage) num = num * 100;
279 
280         nStr = useFixedPrecision ? toFixed(Math.abs(getRounded(num)), decimalPrecision) : Double.toString(num);
281 
282         nStr = showGrouping ? addSeparators(nStr) : nStr.replaceAll("//" + PERIOD, decimalSeparator);
283 
284         String c0 = "";
285         String n0 = "";
286         String c1 = "";
287         String n1 = "";
288         String n2 = "";
289         String c2 = "";
290         String n3 = "";
291         String c3 = "";
292 
293         String negSignL = "" + ((negativeFormat == NEG_PARENTHESIS) ? LEFT_PAREN : DASH);
294         String negSignR = "" + ((negativeFormat == NEG_PARENTHESIS) ? RIGHT_PAREN : DASH);
295         
296         if (currencySymbolPosition == CUR_POS_LEFT_OUTSIDE) {
297             if (num < 0) {
298                 if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
299                     n1 = negSignL;
300                 if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
301                     n2 = negSignR;
302             }
303             if (showCurrencySymbol) c0 = currencySymbol;
304         }
305         else if (currencySymbolPosition == CUR_POS_LEFT_INSIDE) {
306             if (num < 0) {
307                 if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
308                     n0 = negSignL;
309                 if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
310                     n3 = negSignR;
311             }
312             if (showCurrencySymbol) c1 = currencySymbol;
313         }
314         else if (currencySymbolPosition == CUR_POS_RIGHT_INSIDE) {
315             if (num < 0) {
316                 if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
317                     n0 = negSignL;
318                 if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
319                     n3 = negSignR;
320             }
321             if (showCurrencySymbol) c2 = currencySymbol;
322         }
323         else if (currencySymbolPosition == CUR_POS_RIGHT_OUTSIDE) {
324             if (num < 0) {
325                 if (negativeFormat == NEG_LEFT_DASH || negativeFormat == NEG_PARENTHESIS)
326                     n1 = negSignL;
327                 if (negativeFormat == NEG_RIGHT_DASH || negativeFormat == NEG_PARENTHESIS)
328                     n2 = negSignR;
329             }
330             if (showCurrencySymbol) c3 = currencySymbol;
331         }
332         nStr = c0 + n0 + c1 + n1 + nStr + n2 + c2 + n3 + c3 + (isPercentage ? "%" : "");
333 
334         if (isNegativeRed && num < 0) {
335             nStr = "<font color='red'>" + nStr + "</font>";
336         }
337 
338         return nStr;
339    }
340 
341     /***
342      * javascript only rounds to whole numbers, so we need to shift our decimal right, 
343      * then round, then shift the decimal back left.
344      * @param val
345      * @return
346      */
347     private double getRounded (double val)
348     {
349         double exp = Math.pow(10, decimalPrecision);
350         double rounded = val * exp;
351         if (truncate)
352             rounded = rounded >= 0 ? Math.floor(rounded) : Math.ceil(rounded);
353         else
354             rounded = Math.round(rounded);
355         return rounded / exp;
356     }
357 
358     private static native String toFixed(double val, int places) /*-{
359         return val.toFixed(places);
360     }-*/;
361 
362     private static double asNumber(String val, String inputDecimalValue)
363     {
364         String newVal = val;
365         boolean isPercentage = false;
366         // remove % if there is one
367         if (newVal.indexOf('%') != -1) {
368             newVal = newVal.replaceAll("//%", "");
369             isPercentage = true;
370         }
371 
372         // convert abbreviations for thousand, million and billion
373         newVal = newVal.toLowerCase().replaceAll(BILLION, "000000000");
374         newVal = newVal.replaceAll(MILLION, "000000");
375         newVal = newVal.replaceAll(THOUSAND, "000");
376 
377         // remove any characters that are not digit, decimal separator, +, -, (, ), e, or E      
378         String re = "[^//" + inputDecimalValue + "//d//-//+//(//)eE]";
379         newVal = newVal.replaceAll(re, "");
380 
381         // ensure that the first decimal separator is a . and remove the rest.
382         int index = newVal.indexOf(inputDecimalValue);
383         if (index != -1) {
384             newVal = newVal.substring(0, index)
385                     + PERIOD
386                     + (newVal.substring(index + inputDecimalValue.length()).replaceAll("//"
387                             + inputDecimalValue, ""));
388         }
389 
390         // convert right dash and paren negatives to left dash negative
391         if (newVal.charAt(newVal.length() - 1) == DASH) {
392             newVal = newVal.substring(0, newVal.length() - 1);
393             newVal = DASH + newVal;
394         }
395         else if (newVal.charAt(0) == LEFT_PAREN
396                 && newVal.charAt(newVal.length() - 1) == RIGHT_PAREN) {
397             newVal = newVal.substring(1, newVal.length() - 1);
398             newVal = DASH + newVal;
399         }
400 
401         Double parsed;
402         try {
403             parsed = new Double(newVal);
404             if (parsed.isInfinite() || parsed.isNaN()) parsed = new Double(0);
405         }
406         catch (NumberFormatException e) {
407             parsed = new Double(0);
408         }
409 
410         return isPercentage ? parsed.doubleValue() / 100 : parsed.doubleValue();
411     }
412 }
413