1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package net.sf.morph.transform.converters;
17
18 import java.math.BigDecimal;
19 import java.text.DecimalFormatSymbols;
20 import java.text.NumberFormat;
21 import java.text.ParsePosition;
22 import java.util.Locale;
23
24 import net.sf.composite.util.ObjectUtils;
25 import net.sf.morph.Defaults;
26 import net.sf.morph.transform.Converter;
27 import net.sf.morph.transform.DecoratedConverter;
28 import net.sf.morph.transform.TransformationException;
29 import net.sf.morph.transform.transformers.BaseTransformer;
30
31 import org.apache.commons.logging.Log;
32 import org.apache.commons.logging.LogFactory;
33
34 /**
35 * Converts basic text types into primitive numbers or {@link java.lang.Number}
36 * objects.
37 *
38 * @author Matt Sgarlata
39 * @since Jan 4, 2005
40 */
41 public class TextToNumberConverter extends BaseTransformer implements DecoratedConverter {
42
43 private static final char RIGHT_PARENTHESES = ')';
44
45 private static final char LEFT_PARENTHESES = '(';
46
47 private static final Log logger = LogFactory.getLog(TextToNumberConverter.class);
48
49 /**
50 * Constant indicating whitespace characters should be ignored when
51 * converting text to numbers. This is the default treatment of whitespace
52 * characters by this converter.
53 */
54 public static final int WHITESPACE_IGNORE = 0;
55
56 /**
57 * Constant indicating the presence of whitespace characters in text should
58 * prevent conversion of the text into a number (i.e. a
59 * TransformationException will be thrown if whitespace is in the text).
60 */
61 public static final int WHITESPACE_REJECT = 1;
62
63 /**
64 * Constant indicating currency symbols should be ignored when converting
65 * text to numbers. This is the default treatment of currency symbols by
66 * this converter.
67 */
68 public static final int CURRENCY_IGNORE = 0;
69
70 /**
71 * Constant indicating the presence of currency symbols in text should
72 * prevent conversion of the text into a number (i.e. a
73 * TransformationException will be thrown if currency symbols are present in
74 * the text).
75 */
76 public static final int CURRENCY_REJECT = 1;
77
78 /**
79 * Constant indicating percentage symbols should be ignored when converting
80 * text to numbers.
81 */
82 public static final int PERCENTAGE_IGNORE = 0;
83
84 /**
85 * Constant indicating the presence of percentage symbols in text should
86 * prevent conversion of the text into a number (i.e. a
87 * TransformationException will be thrown if percentage symbols are present
88 * in the text).
89 */
90 public static final int PERCENTAGE_REJECT = 1;
91
92 /**
93 * Constant indicating a percentage symbol at the end of text should cause
94 * the text to be treated as a percentage and converted into a corresponding
95 * decimal number. For example, 10 would be converted to 10 and 10% would be
96 * converted to .10. If there are percentage symbols in any position other
97 * than the last character of the text, a TransformationException will be
98 * thrown. This is the default treatment of percentages by this converter.
99 */
100 public static final int PERCENTAGE_CONVERT_TO_DECIMAL = 2;
101
102 /**
103 * Constant indicating parentheses should be ignored when converting
104 * text to numbers.
105 */
106 public static final int PARENTHESES_IGNORE = 0;
107
108 /**
109 * Constant indicating the presence of parentheses in text should
110 * prevent conversion of the text into a number (i.e. a
111 * TransformationException will be thrown if parentheses are present
112 * in the text).
113 */
114 public static final int PARENTHESES_REJECT = 1;
115
116 /**
117 * Constant indicating parentheses enclosing a number should cause the text
118 * to be treated as a negative number. For example, 10 would be converted to
119 * 10 and (10) would be converted to -10. If there are parentheses in
120 * positions other than the first or last character of the text, a
121 * TransformationException will be thrown. This is the default treatment of
122 * parentheses by this converter.
123 */
124 public static final int PARENTHESES_NEGATE = 2;
125
126 /**
127 * The converter used to convert text types from one type to another.
128 */
129 private Converter textConverter;
130 /**
131 * The converter used to convert number types from one type to another.
132 */
133 private Converter numberConverter;
134
135 /**
136 * Configuration option indicating how whitespace should be treated
137 * by this converter. Default is {@link #WHITESPACE_IGNORE}.
138 */
139 private int whitespaceHandling = WHITESPACE_IGNORE;
140 /**
141 * Configuration option indicating how currencies should be treated by
142 * this converter. Default is {@link #CURRENCY_IGNORE}.
143 */
144 private int currencyHandling = CURRENCY_IGNORE;
145 /**
146 * Configuration option indicating how percentages should be treated by
147 * this converter. Default is {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
148 */
149 private int percentageHandling = PERCENTAGE_CONVERT_TO_DECIMAL;
150 /**
151 * Configuration option indicating how parantheses should be treated by
152 * this converter. Default is {@link #PARENTHESES_NEGATE}.
153 */
154 private int parenthesesHandling = PARENTHESES_NEGATE;
155
156 /**
157 * {@inheritDoc}
158 */
159 protected Object convertImpl(Class destinationClass, Object source,
160 Locale locale) throws Exception {
161
162 if (ObjectUtils.isEmpty(source)) {
163 return null;
164 }
165
166 String string = (String) getTextConverter().convert(String.class,
167 source, locale);
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182 StringBuffer charactersToParse =
183
184
185 removeIgnoredCharacters(string, locale);
186
187
188
189 boolean negate = handleParenthesesNegation(charactersToParse, locale);
190 negate = negate || handleNegativeSignNegation(charactersToParse, locale);
191
192 NumberFormat format = null;
193 ParsePosition position = null;
194 Number number = null;
195 Object returnVal = null;
196 String stringToParse = charactersToParse.toString();
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220 if (getPercentageHandling() == PERCENTAGE_CONVERT_TO_DECIMAL) {
221 format = NumberFormat.getPercentInstance(locale);
222 position = new ParsePosition(0);
223 number = format.parse(stringToParse, position);
224 if (isParseSuccessful(stringToParse, position)) {
225
226 returnVal = negateIfNecessary(number, negate, locale);
227
228 returnVal = getNumberConverter().convert(destinationClass, returnVal,
229 locale);
230 if (logger.isDebugEnabled()) {
231 logger.debug("Successfully parsed '" + source + "' as a percentage with value " + returnVal);
232 }
233 return returnVal;
234 }
235 if (logger.isDebugEnabled()) {
236 logger.debug("Could not perform conversion of '" + source + "' by treating the source as a percentage");
237 }
238 }
239
240
241 format = NumberFormat.getInstance(locale);
242 position = new ParsePosition(0);
243 number = format.parse(stringToParse, position);
244 if (isParseSuccessful(stringToParse, position)) {
245
246 returnVal = negateIfNecessary(number, negate, locale);
247
248 returnVal = getNumberConverter().convert(destinationClass, returnVal,
249 locale);
250 if (logger.isDebugEnabled()) {
251 logger.debug("Successfully parsed '" + source + "' as a number or currency value of " + returnVal);
252 }
253 return returnVal;
254 }
255 if (logger.isDebugEnabled()) {
256 logger.debug("Could not perform conversion of '" + source + "' by treating the source as a regular number or currency value");
257 }
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275 throw new TransformationException(destinationClass, source);
276 }
277
278 /**
279 * Negate if necessary
280 * @param returnVal
281 * @param negate
282 * @param locale
283 * @return negated value if negate, else returnValue
284 */
285 private Object negateIfNecessary(Number returnVal, boolean negate, Locale locale) {
286 if (negate) {
287 BigDecimal bd = (BigDecimal) getNumberConverter().convert(BigDecimal.class, returnVal, locale);
288 return bd.negate();
289 }
290 return returnVal;
291 }
292
293 /**
294 * Remove any characters that should be ignored when performing the
295 * conversion.
296 *
297 * @param string
298 * the input string
299 * @param locale
300 * the locale
301 * @return <code>string</code>, with all characters that should be
302 * ignored removed
303 */
304 private StringBuffer removeIgnoredCharacters(String string, Locale locale) {
305 StringBuffer charactersToParse = new StringBuffer();
306 for (int i = 0; i < string.length(); i++) {
307 char currentChar = string.charAt(i);
308 if (getWhitespaceHandling() == WHITESPACE_IGNORE
309 && Character.isWhitespace(currentChar)) {
310 continue;
311 }
312 if (getCurrencyHandling() == CURRENCY_IGNORE
313 && Character.getType(currentChar) == Character.CURRENCY_SYMBOL) {
314 continue;
315 }
316 if (getPercentageHandling() == PERCENTAGE_IGNORE) {
317 DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
318 if (currentChar == symbols.getPercent()) {
319 continue;
320 }
321 }
322 if (getParenthesesHandling() == PARENTHESES_IGNORE) {
323 if (currentChar == LEFT_PARENTHESES
324 || currentChar == RIGHT_PARENTHESES) {
325 continue;
326 }
327 }
328 charactersToParse.append(currentChar);
329 }
330 return charactersToParse;
331 }
332
333 /**
334 * Determines whether negation of the conversion result is needed based on
335 * the presence and handling method of parentheses.
336 *
337 * @param charactersToParse
338 * the characters to parse
339 * @param locale
340 * the locale
341 * @return <code>true</code>, if the number is enclosed by parantheses
342 * and parantheses handling is set to PARENTHESES_NEGATE or<br>
343 * <code>false</code>, otherwise
344 */
345 private boolean handleParenthesesNegation(StringBuffer charactersToParse, Locale locale) {
346 int lastCharIndex = charactersToParse.length() - 1;
347
348
349 if (getParenthesesHandling() == PARENTHESES_NEGATE &&
350 charactersToParse.charAt(0) == LEFT_PARENTHESES &&
351 charactersToParse.charAt(lastCharIndex) == RIGHT_PARENTHESES) {
352
353 charactersToParse.deleteCharAt(lastCharIndex);
354
355 charactersToParse.deleteCharAt(0);
356
357 return true;
358 }
359
360 return false;
361 }
362
363 /**
364 * Determines whether negation of the conversion result is needed based on
365 * the presence of the minus sign character.
366 *
367 * @param charactersToParse
368 * the characters to parse
369 * @param locale
370 * the locale
371 * @return <code>true</code>, if the number is enclosed by parantheses
372 * and parantheses handling is set to PARENTHESES_NEGATE or<br>
373 * <code>false</code>, otherwise
374 */
375 private boolean handleNegativeSignNegation(StringBuffer charactersToParse, Locale locale) {
376 if (charactersToParse.charAt(0) == '-') {
377 charactersToParse.deleteCharAt(0);
378 return true;
379 }
380 if (charactersToParse.charAt(charactersToParse.length() - 1) == '-') {
381 charactersToParse.deleteCharAt(charactersToParse.length() - 1);
382 return true;
383 }
384 return false;
385 }
386
387 /**
388 * Learn whether the entire string was consumed.
389 * @param stringWithoutIgnoredSymbolsStr
390 * @param position
391 * @return boolean
392 */
393 protected boolean isParseSuccessful(String stringWithoutIgnoredSymbolsStr, ParsePosition position) {
394 return position.getIndex() != 0 &&
395 position.getIndex() == stringWithoutIgnoredSymbolsStr.length();
396 }
397
398 /**
399 * {@inheritDoc}
400 */
401 protected boolean isWrappingRuntimeExceptions() {
402 return true;
403 }
404
405 /**
406 * {@inheritDoc}
407 */
408 protected Class[] getSourceClassesImpl() throws Exception {
409 return getTextConverter().getSourceClasses();
410 }
411
412 /**
413 * {@inheritDoc}
414 */
415 protected Class[] getDestinationClassesImpl() throws Exception {
416 return getNumberConverter().getDestinationClasses();
417 }
418
419 /**
420 * Sets the converter used to convert text types from one type to another.
421 *
422 * @return the converter used to convert text types from one type to another
423 */
424 public Converter getNumberConverter() {
425 if (numberConverter == null) {
426 setNumberConverter(Defaults.createNumberConverter());
427 }
428 return numberConverter;
429 }
430
431 /**
432 * Sets the converter used to convert text types from one type to another.
433 *
434 * @param numberConverter
435 * the converter used to convert text types from one type to
436 * another
437 */
438 public void setNumberConverter(Converter numberConverter) {
439 this.numberConverter = numberConverter;
440 }
441
442 /**
443 * Gets the converter used to convert text types from one type to another.
444 *
445 * @return the converter used to convert text types from one type to another
446 */
447 public Converter getTextConverter() {
448 if (textConverter == null) {
449 setTextConverter(Defaults.createTextConverter());
450 }
451 return textConverter;
452 }
453
454 /**
455 * Sets the converter used to convert text types from one type to another.
456 *
457 * @param textConverter
458 * the converter used to convert text types from one type to
459 * another
460 */
461 public void setTextConverter(Converter textConverter) {
462 this.textConverter = textConverter;
463 }
464
465 /**
466 * Retrieves the configuration option indicating how currencies should be
467 * treated by this converter. Default is {@link #CURRENCY_IGNORE}.
468 *
469 * @return the configuration option indicating how currencies should be
470 * treated by this converter
471 */
472 public int getCurrencyHandling() {
473 return currencyHandling;
474 }
475
476 /**
477 * Sets the configuration option indicating how currencies should be treated
478 * by this converter. Default is {@link #CURRENCY_IGNORE}.
479 *
480 * @param currencyHandling
481 * the configuration option indicating how currencies should be
482 * treated by this converter. Default is {@link #CURRENCY_IGNORE}.
483 */
484 public void setCurrencyHandling(int currencyHandling) {
485 this.currencyHandling = currencyHandling;
486 }
487
488 /**
489 * Retrieves the configuration option indicating how parantheses should be
490 * treated by this converter. Default is {@link #PARENTHESES_NEGATE}.
491 *
492 * @return the configuration option indicating how parantheses should be
493 * treated by this converter. Default is {@link #PARENTHESES_NEGATE}.
494 */
495 public int getParenthesesHandling() {
496 return parenthesesHandling;
497 }
498
499 /**
500 * Sets the configuration option indicating how parantheses should be
501 * treated by this converter. Default is {@link #PARENTHESES_NEGATE}.
502 *
503 * @param parenthesesHandling
504 * the configuration option indicating how parantheses should be
505 * treated by this converter. Default is
506 * {@link #PARENTHESES_NEGATE}.
507 */
508 public void setParenthesesHandling(int parenthesesHandling) {
509 this.parenthesesHandling = parenthesesHandling;
510 }
511
512 /**
513 * Gets the configuration option indicating how percentages should be treated by
514 * this converter. Default is {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
515 * @return the configuration option indicating how percentages should be treated by
516 * this converter. Default is {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
517 */
518 public int getPercentageHandling() {
519 return percentageHandling;
520 }
521
522 /**
523 * Sets the configuration option indicating how percentages should be
524 * treated by this converter. Default is
525 * {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
526 *
527 * @param percentageHandling
528 * the configuration option indicating how percentages should be
529 * treated by this converter. Default is
530 * {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
531 */
532 public void setPercentageHandling(int percentageHandling) {
533 this.percentageHandling = percentageHandling;
534 }
535
536 /**
537 * Gets the configuration option indicating how whitespace should be treated
538 * by this converter. Default is {@link #WHITESPACE_IGNORE}.
539 *
540 * @return the configuration option indicating how whitespace should be
541 * treated by this converter. Default is {@link #WHITESPACE_IGNORE}.
542 */
543 public int getWhitespaceHandling() {
544 return whitespaceHandling;
545 }
546
547 /**
548 * Sets the configuration option indicating how whitespace should be treated
549 * by this converter. Default is {@link #WHITESPACE_IGNORE}.
550 *
551 * @param whitespaceHandling
552 * the configuration option indicating how whitespace should be
553 * treated by this converter. Default is
554 * {@link #WHITESPACE_IGNORE}.
555 */
556 public void setWhitespaceHandling(int whitespaceHandling) {
557 this.whitespaceHandling = whitespaceHandling;
558 }
559
560 }