001: package org.apache.velocity.tools.generic;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.util.Arrays;
023: import java.util.Collection;
024: import java.util.Iterator;
025: import org.apache.commons.beanutils.PropertyUtils;
026:
027: /**
028: * <p>Tool for performing math in Velocity.</p>
029: *
030: * <p>Some things should be noted here:</p>
031: * <ul>
032: * <li>This class does not have methods that take
033: * primitives. This is simply because Velocity
034: * wraps all primitives for us automagically.</li>
035: *
036: * <li>No null pointer, number format, or divide by zero
037: * exceptions are thrown here. This is because such exceptions
038: * thrown in template halt rendering. It should be sufficient
039: * debugging feedback that Velocity will render the reference
040: * literally. (e.g. $math.div(1, 0) renders as '$math.div(1, 0)')</li>
041: * </ul>
042: * <p><pre>
043: * Example toolbox.xml config (if you want to use this with VelocityView):
044: * <tool>
045: * <key>math</key>
046: * <scope>application</scope>
047: * <class>org.apache.velocity.tools.generic.MathTool</class>
048: * </tool>
049: * </pre></p>
050: *
051: * @author Nathan Bubna
052: * @author Leon Messerschmidt
053: * @version $Revision: 479724 $ $Date: 2006-11-27 10:49:37 -0800 (Mon, 27 Nov 2006) $
054: */
055: public class MathTool {
056: /**
057: * @param num1 the first number
058: * @param num2 the second number
059: * @return the sum of the numbers or
060: * <code>null</code> if they're invalid
061: * @see #toNumber
062: */
063: public Number add(Object num1, Object num2) {
064: Number n1 = toNumber(num1);
065: Number n2 = toNumber(num2);
066: if (n1 == null || n2 == null) {
067: return null;
068: }
069: double value = n1.doubleValue() + n2.doubleValue();
070: return matchType(n1, n2, value);
071: }
072:
073: /**
074: * @param num1 the first number
075: * @param num2 the second number
076: * @return the difference of the numbers or
077: * <code>null</code> if they're invalid
078: * @see #toNumber
079: */
080: public Number sub(Object num1, Object num2) {
081: Number n1 = toNumber(num1);
082: Number n2 = toNumber(num2);
083: if (n1 == null || n2 == null) {
084: return null;
085: }
086: double value = n1.doubleValue() - n2.doubleValue();
087: return matchType(n1, n2, value);
088: }
089:
090: /**
091: * @param num1 the first number
092: * @param num2 the second number
093: * @return the product of the numbers or
094: * <code>null</code> if they're invalid
095: * @see #toNumber
096: */
097: public Number mul(Object num1, Object num2) {
098: Number n1 = toNumber(num1);
099: Number n2 = toNumber(num2);
100: if (n1 == null || n2 == null) {
101: return null;
102: }
103: double value = n1.doubleValue() * n2.doubleValue();
104: return matchType(n1, n2, value);
105: }
106:
107: /**
108: * @param num1 the first number
109: * @param num2 the second number
110: * @return the quotient of the numbers or
111: * <code>null</code> if they're invalid
112: * @see #toNumber
113: */
114: public Number div(Object num1, Object num2) {
115: Number n1 = toNumber(num1);
116: Number n2 = toNumber(num2);
117: if (n1 == null || n2 == null || n2.doubleValue() == 0.0) {
118: return null;
119: }
120: double value = n1.doubleValue() / n2.doubleValue();
121: return matchType(n1, n2, value);
122: }
123:
124: /**
125: * @param num1 the first number
126: * @param num2 the second number
127: * @return the first number raised to the power of the
128: * second or <code>null</code> if they're invalid
129: * @see #toNumber
130: */
131: public Number pow(Object num1, Object num2) {
132: Number n1 = toNumber(num1);
133: Number n2 = toNumber(num2);
134: if (n1 == null || n2 == null) {
135: return null;
136: }
137: double value = Math.pow(n1.doubleValue(), n2.doubleValue());
138: return matchType(n1, n2, value);
139: }
140:
141: /**
142: * Does integer division on the int values of the specified numbers.
143: *
144: * <p>So, $math.idiv('5.1',3) will return '1',
145: * and $math.idiv(6,'3.9') will return '2'.</p>
146: *
147: * @param num1 the first number
148: * @param num2 the second number
149: * @return the result of performing integer division
150: * on the operands.
151: * @see #toInteger
152: */
153: public Integer idiv(Object num1, Object num2) {
154: Number n1 = toNumber(num1);
155: Number n2 = toNumber(num2);
156: if (n1 == null || n2 == null || n2.intValue() == 0) {
157: return null;
158: }
159: int value = n1.intValue() / n2.intValue();
160: return new Integer(value);
161: }
162:
163: /**
164: * Does integer modulus on the int values of the specified numbers.
165: *
166: * <p>So, $math.mod('5.1',3) will return '2',
167: * and $math.mod(6,'3.9') will return '0'.</p>
168: *
169: * @param num1 the first number
170: * @param num2 the second number
171: * @return the result of performing integer modulus
172: * on the operands.
173: * @see #toInteger
174: */
175: public Integer mod(Object num1, Object num2) {
176: Number n1 = toNumber(num1);
177: Number n2 = toNumber(num2);
178: if (n1 == null || n2 == null || n2.intValue() == 0) {
179: return null;
180: }
181: int value = n1.intValue() % n2.intValue();
182: return new Integer(value);
183: }
184:
185: /**
186: * @param num1 the first number
187: * @param num2 the second number
188: * @return the largest of the numbers or
189: * <code>null</code> if they're invalid
190: * @see #toNumber
191: */
192: public Number max(Object num1, Object num2) {
193: Number n1 = toNumber(num1);
194: Number n2 = toNumber(num2);
195: if (n1 == null || n2 == null) {
196: return null;
197: }
198: double value = Math.max(n1.doubleValue(), n2.doubleValue());
199: return matchType(n1, n2, value);
200: }
201:
202: /**
203: * @param num1 the first number
204: * @param num2 the second number
205: * @return the smallest of the numbers or
206: * <code>null</code> if they're invalid
207: * @see #toNumber
208: */
209: public Number min(Object num1, Object num2) {
210: Number n1 = toNumber(num1);
211: Number n2 = toNumber(num2);
212: if (n1 == null || n2 == null) {
213: return null;
214: }
215: double value = Math.min(n1.doubleValue(), n2.doubleValue());
216: return matchType(n1, n2, value);
217: }
218:
219: /**
220: * @param num the number
221: * @return the absolute value of the number or
222: * <code>null</code> if it's invalid
223: * @see #toDouble
224: */
225: public Number abs(Object num) {
226: Number n = toNumber(num);
227: if (n == null) {
228: return null;
229: }
230: double value = Math.abs(n.doubleValue());
231: return matchType(n, value);
232: }
233:
234: /**
235: * @param num the number
236: * @return the smallest integer that is not
237: * less than the given number
238: */
239: public Integer ceil(Object num) {
240: Number n = toNumber(num);
241: if (n == null) {
242: return null;
243: }
244: return new Integer((int) Math.ceil(n.doubleValue()));
245: }
246:
247: /**
248: * @param num the number
249: * @return the integer portion of the number
250: */
251: public Integer floor(Object num) {
252: Number n = toNumber(num);
253: if (n == null) {
254: return null;
255: }
256: return new Integer((int) Math.floor(n.doubleValue()));
257: }
258:
259: /**
260: * Rounds a number to the nearest whole Integer
261: *
262: * @param num the number to round
263: * @return the number rounded to the nearest whole Integer
264: * or <code>null</code> if it's invalid
265: * @see java.lang.Math#rint(double)
266: */
267: public Integer round(Object num) {
268: Number n = toNumber(num);
269: if (n == null) {
270: return null;
271: }
272: return new Integer((int) Math.rint(n.doubleValue()));
273: }
274:
275: /**
276: * Rounds a number to the specified number of decimal places.
277: * This is particulary useful for simple display formatting.
278: * If you want to round an number to the nearest integer, it
279: * is better to use {@link #round}, as that will return
280: * an {@link Integer} rather than a {@link Double}.
281: *
282: * @param decimals the number of decimal places
283: * @param num the number to round
284: * @return the value rounded to the specified number of
285: * decimal places or <code>null</code> if it's invalid
286: * @see #toNumber
287: */
288: public Double roundTo(Object decimals, Object num) {
289: Number i = toNumber(decimals);
290: Number d = toNumber(num);
291: if (i == null || d == null) {
292: return null;
293: }
294: //ok, go ahead and do the rounding
295: int places = i.intValue();
296: double value = d.doubleValue();
297: if (places == 0) {
298: value = (int) (value + .5);
299: } else {
300: double shift = Math.pow(10, places);
301: value = value * shift;
302: value = (int) (value + .5);
303: value = value / shift;
304: }
305: return new Double(value);
306: }
307:
308: /**
309: * @return a pseudo-random {@link Double} greater
310: * than or equal to 0.0 and less than 1.0
311: * @see Math#random()
312: */
313: public Double getRandom() {
314: return new Double(Math.random());
315: }
316:
317: /**
318: * This returns a random {@link Number} within the
319: * specified range. The returned value will be
320: * greater than or equal to the first number
321: * and less than the second number. If both arguments
322: * are whole numbers then the returned number will
323: * also be, otherwise a {@link Double} will
324: * be returned.
325: *
326: * @param num1 the first number
327: * @param num2 the second number
328: * @return a pseudo-random {@link Number} greater than
329: * or equal to the first number and less than
330: * the second
331: * @see Math#random()
332: */
333: public Number random(Object num1, Object num2) {
334: Number n1 = toNumber(num1);
335: Number n2 = toNumber(num2);
336: if (n1 == null || n2 == null) {
337: return null;
338: }
339:
340: double diff = n2.doubleValue() - n1.doubleValue();
341: // multiply the difference by a pseudo-random double from
342: // 0.0 to 1.0, round to the nearest int, and add the first
343: // value to the random int and return as an Integer
344: double random = (diff * Math.random()) + n1.doubleValue();
345:
346: // check if either of the args were floating points
347: String in = n1.toString() + n2.toString();
348: if (in.indexOf('.') < 0) {
349: // args were whole numbers, so return the same
350: return matchType(n1, n2, Math.floor(random));
351: }
352: // one of the args was a floating point,
353: // so don't floor the result
354: return new Double(random);
355: }
356:
357: // --------------- public type conversion methods ---------
358:
359: /**
360: * Converts an object with a numeric value into an Integer
361: * Valid formats are {@link Number} or a {@link String}
362: * representation of a number
363: *
364: * @param num the number to be converted
365: * @return a {@link Integer} representation of the number
366: * or <code>null</code> if it's invalid
367: */
368: public Integer toInteger(Object num) {
369: Number n = toNumber(num);
370: if (n == null) {
371: return null;
372: }
373: return new Integer(n.intValue());
374: }
375:
376: /**
377: * Converts an object with a numeric value into a Double
378: * Valid formats are {@link Number} or a {@link String}
379: * representation of a number
380: *
381: * @param num the number to be converted
382: * @return a {@link Double} representation of the number
383: * or <code>null</code> if it's invalid
384: */
385: public Double toDouble(Object num) {
386: Number n = toNumber(num);
387: if (n == null) {
388: return null;
389: }
390: return new Double(n.doubleValue());
391: }
392:
393: /**
394: * Converts an object with a numeric value into a Number
395: * Valid formats are {@link Number} or a {@link String}
396: * representation of a number. Note that this does not
397: * handle localized number formats. Use the {@link NumberTool}
398: * to handle such conversions.
399: *
400: * @param num the number to be converted
401: * @return a {@link Number} representation of the number
402: * or <code>null</code> if it's invalid
403: */
404: public Number toNumber(Object num) {
405: if (num == null) {
406: return null;
407: }
408: if (num instanceof Number) {
409: return (Number) num;
410: }
411: try {
412: return parseNumber(String.valueOf(num));
413: } catch (NumberFormatException nfe) {
414: return null;
415: }
416: }
417:
418: // --------------------------- protected methods ------------------
419:
420: /**
421: * @see #matchType(Number,Number,double)
422: */
423: protected Number matchType(Number in, double out) {
424: return matchType(in, null, out);
425: }
426:
427: /**
428: * Takes the original argument(s) and returns the resulting value as
429: * an instance of the best matching type (Integer, Long, or Double).
430: * If either an argument or the result is not an integer (i.e. has no
431: * decimal when rendered) the result will be returned as a Double.
432: * If not and the result is < -2147483648 or > 2147483647, then a
433: * Long will be returned. Otherwise, an Integer will be returned.
434: */
435: protected Number matchType(Number in1, Number in2, double out) {
436: //NOTE: if we just checked class types, we could miss custom
437: // extensions of java.lang.Number, and if we only checked
438: // the mathematical value, $math.div('3.0', 1) would render
439: // as '3'. To get the expected result, we check what we're
440: // concerned about: the rendered string.
441:
442: // first check if the result is a whole number
443: boolean isWhole = (Math.rint(out) == out);
444:
445: if (isWhole) {
446: // assume that 1st arg is not null,
447: // check for floating points
448: String in = in1.toString();
449: isWhole = (in.indexOf('.') < 0);
450:
451: // if we don't have a decimal yet but do have a second arg
452: if (isWhole && in2 != null) {
453: in = in2.toString();
454: isWhole = (in.indexOf('.') < 0);
455: }
456: }
457:
458: if (!isWhole) {
459: return new Double(out);
460: } else if (out > Integer.MAX_VALUE || out < Integer.MIN_VALUE) {
461: return new Long((long) out);
462: } else {
463: return new Integer((int) out);
464: }
465: }
466:
467: /**
468: * Converts an object into a {@link Number} (if it can)
469: * This is used as the base for all numeric parsing methods. So,
470: * sub-classes can override to allow for customized number parsing.
471: * (e.g. for i18n, fractions, compound numbers, bigger numbers, etc.)
472: *
473: * @param value the string to be parsed
474: * @return the value as a {@link Number}
475: */
476: protected Number parseNumber(String value)
477: throws NumberFormatException {
478: // check for the floating point
479: if (value.indexOf('.') < 0) {
480: // check for large numbers
481: long i = new Long(value).longValue();
482: if (i > Integer.MAX_VALUE || i < Integer.MIN_VALUE) {
483: return new Long(i);
484: } else {
485: return new Integer((int) i);
486: }
487: } else {
488: return new Double(value);
489: }
490: }
491:
492: // ------------------------- Aggregation methods ------------------
493:
494: /**
495: * Get the sum of the values from a list
496: *
497: * @param collection A collection containing Java beans
498: * @param field A Java Bean field for the objects in <i>collection</i> that
499: * will return a number.
500: * @return The sum of the values in <i>collection</i>.
501: */
502: public Number getTotal(Collection collection, String field) {
503: if (collection == null || field == null) {
504: return null;
505: }
506: try {
507: double result = 0;
508: // hold the first number and use it to match return type
509: Number first = null;
510: for (Iterator i = collection.iterator(); i.hasNext();) {
511: Object property = PropertyUtils.getProperty(i.next(),
512: field);
513: Number value = toNumber(property);
514: if (first == null) {
515: first = value;
516: }
517: result += value.doubleValue();
518: }
519: return matchType(first, result);
520: } catch (Exception e) {
521: //FIXME? Log this?
522: return null;
523: }
524: }
525:
526: /**
527: * Get the average of the values from a list
528: *
529: * @param collection A collection containing Java beans
530: * @param field A Java Bean field for the objects in <i>collection</i> that
531: * will return a number.
532: * @return The average of the values in <i>collection</i>.
533: */
534: public Number getAverage(Collection collection, String field) {
535: Number result = getTotal(collection, field);
536: if (result == null) {
537: return null;
538: }
539: double avg = result.doubleValue() / collection.size();
540: return matchType(result, avg);
541: }
542:
543: /**
544: * Get the sum of the values from a list
545: *
546: * @param array An array containing Java beans
547: * @param field A Java Bean field for the objects in <i>array</i> that
548: * will return a number.
549: * @return The sum of the values in <i>array</i>.
550: */
551: public Number getTotal(Object[] array, String field) {
552: return getTotal(Arrays.asList(array), field);
553: }
554:
555: /**
556: * Get the sum of the values from a list
557: *
558: * @param array A collection containing Java beans
559: * @param field A Java Bean field for the objects in <i>array</i> that
560: * will return a number.
561: * @return The sum of the values in <i>array</i>.
562: */
563: public Number getAverage(Object[] array, String field) {
564: return getAverage(Arrays.asList(array), field);
565: }
566:
567: /**
568: * Get the sum of the values
569: *
570: * @param collection A collection containing numeric values
571: * @return The sum of the values in <i>collection</i>.
572: */
573: public Number getTotal(Collection collection) {
574: if (collection == null) {
575: return null;
576: }
577:
578: double result = 0;
579: // grab the first number and use it to match return type
580: Number first = null;
581: for (Iterator i = collection.iterator(); i.hasNext();) {
582: Number value = toNumber(i.next());
583: if (value == null) {
584: //FIXME? or should we ignore this and keep adding?
585: return null;
586: }
587: if (first == null) {
588: first = value;
589: }
590: result += value.doubleValue();
591: }
592: return matchType(first, result);
593: }
594:
595: /**
596: * Get the average of the values
597: *
598: * @param collection A collection containing number values
599: * @return The average of the values in <i>collection</i>.
600: */
601: public Number getAverage(Collection collection) {
602: Number result = getTotal(collection);
603: if (result == null) {
604: return null;
605: }
606: double avg = result.doubleValue() / collection.size();
607: return matchType(result, avg);
608: }
609:
610: /**
611: * Get the sum of the values
612: *
613: * @param array An array containing number values
614: * @return The sum of the values in <i>array</i>.
615: */
616: public Number getTotal(Object[] array) {
617: return getTotal(Arrays.asList(array));
618: }
619:
620: /**
621: * Get the average of the values
622: *
623: * @param array An array containing number values
624: * @return The sum of the values in <i>array</i>.
625: */
626: public Number getAverage(Object[] array) {
627: return getAverage(Arrays.asList(array));
628: }
629:
630: /**
631: * Get the sum of the values
632: *
633: * @param values The list of double values to add up.
634: * @return The sum of the arrays
635: */
636: public Number getTotal(double[] values) {
637: if (values == null) {
638: return null;
639: }
640:
641: double result = 0;
642: for (int i = 0; i < values.length; i++) {
643: result += values[i];
644: }
645: return new Double(result);
646: }
647:
648: /**
649: * Get the average of the values in an array of double values
650: *
651: * @param values The list of double values
652: * @return The average of the array of values
653: */
654: public Number getAverage(double[] values) {
655: Number total = getTotal(values);
656: if (total == null) {
657: return null;
658: }
659: return new Double(total.doubleValue() / values.length);
660: }
661:
662: /**
663: * Get the sum of the values
664: *
665: * @param values The list of long values to add up.
666: * @return The sum of the arrays
667: */
668: public Number getTotal(long[] values) {
669: if (values == null) {
670: return null;
671: }
672:
673: long result = 0;
674: for (int i = 0; i < values.length; i++) {
675: result += values[i];
676: }
677: return new Long(result);
678: }
679:
680: /**
681: * Get the average of the values in an array of long values
682: *
683: * @param values The list of long values
684: * @return The average of the array of values
685: */
686: public Number getAverage(long[] values) {
687: Number total = getTotal(values);
688: if (total == null) {
689: return null;
690: }
691: double avg = total.doubleValue() / values.length;
692: return matchType(total, avg);
693: }
694:
695: }
|